merge with latest main

This commit is contained in:
SergeyMenshykh
2026-02-09 13:26:06 +00:00
667 changed files with 27645 additions and 19375 deletions
+66
View File
@@ -0,0 +1,66 @@
# AGENTS.md
Instructions for AI coding agents working in the .NET codebase.
## Build, Test, and Lint Commands
```bash
# From dotnet/ directory
dotnet build # Build all projects
dotnet test # Run all tests
dotnet format # Auto-fix formatting
# Build/test a specific project (preferred for isolated changes)
dotnet build src/Microsoft.Agents.AI.<Package>
dotnet test tests/Microsoft.Agents.AI.<Package>.UnitTests
# Run a single test
dotnet test --filter "FullyQualifiedName~TestClassName.TestMethodName"
```
**Note**: Changes to core packages (`Microsoft.Agents.AI`, `Microsoft.Agents.AI.Abstractions`) affect dependent projects - run checks across the entire solution. For isolated changes, build/test only the affected project to save time.
## Project Structure
```
dotnet/
├── src/
│ ├── Microsoft.Agents.AI/ # Core AI agent abstractions
│ ├── Microsoft.Agents.AI.Abstractions/ # Shared abstractions and interfaces
│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider
│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI provider
│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider
│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration
│ └── ... # Other packages
├── samples/ # Sample applications
└── tests/ # Unit and integration tests
```
### External Dependencies
The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Abstractions` (external NuGet packages) using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, and `AIContent`.
## Key Conventions
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
- **XML docs**: Required for all public methods and classes
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
- **Private classes**: Should be `sealed` unless subclassed
- **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking
## Sample Structure
1. Copyright header: `// Copyright (c) Microsoft. All rights reserved.`
2. Description comment explaining what the sample demonstrates
3. Using statements
4. Main code logic
5. Helper methods at bottom
Configuration via environment variables (never hardcode secrets). Keep samples simple and focused.
When adding a new sample:
- Create a standalone project in `samples/` with matching directory and project names
- Include a README.md explaining what the sample does and how to run it
- Add the project to the solution file
- Reference the sample in the parent directory's README.md
+2 -1
View File
@@ -81,7 +81,7 @@
<Project Path="samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step07_3rdPartyChatHistoryStorage/Agent_Step07_3rdPartyChatHistoryStorage.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step08_Observability/Agent_Step08_Observability.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
@@ -131,6 +131,7 @@
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260128.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260128.1</PackageVersion>
<GitTag>1.0.0-preview.260128.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260205.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260205.1</PackageVersion>
<GitTag>1.0.0-preview.260205.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -28,11 +28,21 @@ namespace SampleApp
{
public override string? Name => "UpperCaseParrotAgent";
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new CustomAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CustomAgentSession(serializedSession, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is not CustomAgentSession typedSession)
{
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
}
return typedSession.Serialize(jsonSerializerOptions);
}
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CustomAgentSession(serializedState, jsonSerializerOptions));
protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
@@ -45,14 +55,14 @@ namespace SampleApp
}
// Get existing messages from the store
var invokingContext = new ChatHistoryProvider.InvokingContext(messages);
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
var storeMessages = await typedSession.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 session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages, storeMessages)
{
ResponseMessages = responseMessages
};
@@ -77,14 +87,14 @@ namespace SampleApp
}
// Get existing messages from the store
var invokingContext = new ChatHistoryProvider.InvokingContext(messages);
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
var storeMessages = await typedSession.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 session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, messages, storeMessages)
{
ResponseMessages = responseMessages
};
@@ -136,6 +146,9 @@ namespace SampleApp
internal CustomAgentSession(JsonElement serializedSessionState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedSessionState, jsonSerializerOptions) { }
internal new JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> base.Serialize(jsonSerializerOptions);
}
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use Anthropic-managed Skills with an AI agent.
// Skills are pre-built capabilities provided by Anthropic that can be used with the Claude API.
// This sample shows how to:
// 1. List available Anthropic-managed skills
// 2. Use the pptx skill to create PowerPoint presentations
// 3. Download and save generated files
using Anthropic;
using Anthropic.Core;
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Files;
using Anthropic.Models.Beta.Messages;
using Anthropic.Models.Beta.Skills;
using Anthropic.Services;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
string apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
// Skills require Claude 4.5 models (Sonnet 4.5, Haiku 4.5, or Opus 4.5)
string model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-sonnet-4-5-20250929";
// Create the Anthropic client
AnthropicClient anthropicClient = new() { ApiKey = apiKey };
// List available Anthropic-managed skills (optional - API may not be available in all regions)
Console.WriteLine("Available Anthropic-managed skills:");
try
{
SkillListPage skills = await anthropicClient.Beta.Skills.List(
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
foreach (var skill in skills.Items)
{
Console.WriteLine($" {skill.Source}: {skill.ID} (version: {skill.LatestVersion})");
}
}
catch (Exception ex)
{
Console.WriteLine($" (Skills listing not available: {ex.Message})");
}
Console.WriteLine();
// Define the pptx skill - the SDK handles all beta flags and container configuration automatically
// when using AsAITool(), so no manual RawRepresentationFactory configuration is needed.
BetaSkillParams pptxSkill = new()
{
Type = BetaSkillParamsType.Anthropic,
SkillID = "pptx",
Version = "latest"
};
// Create an agent with the pptx skill enabled.
// Skills require extended thinking and higher max tokens for complex file generation.
// The SDK's AsAITool() handles beta flags and container config automatically.
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
model: model,
instructions: "You are a helpful agent for creating PowerPoint presentations.",
tools: [pptxSkill.AsAITool()],
clientFactory: (chatClient) => chatClient
.AsBuilder()
.ConfigureOptions(options =>
{
options.RawRepresentationFactory = (_) => new MessageCreateParams()
{
Model = model,
MaxTokens = 20000,
Messages = [],
Thinking = new BetaThinkingConfigParam(
new BetaThinkingConfigEnabled(budgetTokens: 10000))
};
})
.Build());
Console.WriteLine("Creating a presentation about renewable energy...\n");
// Run the agent with a request to create a presentation
AgentResponse response = await agent.RunAsync("Create a simple 3-slide presentation about renewable energy sources. Include a title slide, a slide about solar energy, and a slide about wind energy.");
Console.WriteLine("#### Agent Response ####");
Console.WriteLine(response.Text);
// Display any reasoning/thinking content
List<TextReasoningContent> reasoningContents = response.Messages.SelectMany(m => m.Contents.OfType<TextReasoningContent>()).ToList();
if (reasoningContents.Count > 0)
{
Console.WriteLine("\n#### Agent Reasoning ####");
Console.WriteLine($"\e[92m{string.Join("\n", reasoningContents.Select(c => c.Text))}\e[0m");
}
// Collect generated files from CodeInterpreterToolResultContent outputs
List<HostedFileContent> hostedFiles = response.Messages
.SelectMany(m => m.Contents.OfType<CodeInterpreterToolResultContent>())
.Where(c => c.Outputs is not null)
.SelectMany(c => c.Outputs!.OfType<HostedFileContent>())
.ToList();
if (hostedFiles.Count > 0)
{
Console.WriteLine("\n#### Generated Files ####");
foreach (HostedFileContent file in hostedFiles)
{
Console.WriteLine($" FileId: {file.FileId}");
// Download the file using the Anthropic Files API
using HttpResponse fileResponse = await anthropicClient.Beta.Files.Download(
file.FileId,
new FileDownloadParams { Betas = ["files-api-2025-04-14"] });
// Save the file to disk
string fileName = $"presentation_{file.FileId.Substring(0, 8)}.pptx";
using FileStream fileStream = File.Create(fileName);
Stream contentStream = await fileResponse.ReadAsStream();
await contentStream.CopyToAsync(fileStream);
Console.WriteLine($" Saved to: {fileName}");
}
}
Console.WriteLine("\nToken usage:");
Console.WriteLine($"Input: {response.Usage?.InputTokenCount}, Output: {response.Usage?.OutputTokenCount}");
if (response.Usage?.AdditionalCounts is not null)
{
Console.WriteLine($"Additional: {string.Join(", ", response.Usage.AdditionalCounts)}");
}
@@ -0,0 +1,119 @@
# Using Anthropic Skills with agents
This sample demonstrates how to use Anthropic-managed Skills with AI agents. Skills are pre-built capabilities provided by Anthropic that can be used with the Claude API.
## What this sample demonstrates
- Listing available Anthropic-managed skills
- Creating an AI agent with Anthropic Claude Skills support using the simplified `AsAITool()` approach
- Using the pptx skill to create PowerPoint presentations
- Downloading and saving generated files to disk
- Handling agent responses with generated content
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10.0 SDK or later
- Anthropic API key configured
- Access to Anthropic Claude models with Skills support
**Note**: This sample uses Anthropic Claude models with Skills. Skills are a beta feature. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
Set the following environment variables:
```powershell
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model (e.g., claude-sonnet-4-5-20250929)
```
## Run the sample
Navigate to the AgentWithAnthropic sample directory and run:
```powershell
cd dotnet\samples\GettingStarted\AgentWithAnthropic
dotnet run --project .\Agent_Anthropic_Step04_UsingSkills
```
## Available Anthropic Skills
Anthropic provides several managed skills that can be used with the Claude API:
- `pptx` - Create PowerPoint presentations
- `xlsx` - Create Excel spreadsheets
- `docx` - Create Word documents
- `pdf` - Create and analyze PDF documents
You can list available skills using the Anthropic SDK:
```csharp
SkillListPage skills = await anthropicClient.Beta.Skills.List(
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
foreach (var skill in skills.Items)
{
Console.WriteLine($"{skill.Source}: {skill.ID} (version: {skill.LatestVersion})");
}
```
## Expected behavior
The sample will:
1. List all available Anthropic-managed skills
2. Create an agent with the pptx skill enabled
3. Run the agent with a request to create a presentation
4. Display the agent's response text
5. Download any generated files and save them to disk
6. Display token usage statistics
## Code highlights
### Simplified skill configuration
The Anthropic SDK handles all beta flags and container configuration automatically when using `AsAITool()`:
```csharp
// Define the pptx skill
BetaSkillParams pptxSkill = new()
{
Type = BetaSkillParamsType.Anthropic,
SkillID = "pptx",
Version = "latest"
};
// Create an agent - the SDK handles beta flags automatically!
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
model: model,
instructions: "You are a helpful agent for creating PowerPoint presentations.",
tools: [pptxSkill.AsAITool()]);
```
**Note**: No manual `RawRepresentationFactory`, `Betas`, or `Container` configuration is needed. The SDK automatically adds the required beta headers (`skills-2025-10-02`, `code-execution-2025-08-25`) and configures the container with the skill.
### Handling generated files
Generated files are returned as `HostedFileContent` within `CodeInterpreterToolResultContent`:
```csharp
// Collect generated files from response
List<HostedFileContent> hostedFiles = response.Messages
.SelectMany(m => m.Contents.OfType<CodeInterpreterToolResultContent>())
.Where(c => c.Outputs is not null)
.SelectMany(c => c.Outputs!.OfType<HostedFileContent>())
.ToList();
// Download and save each file
foreach (HostedFileContent file in hostedFiles)
{
using HttpResponse fileResponse = await anthropicClient.Beta.Files.Download(
file.FileId,
new FileDownloadParams { Betas = ["files-api-2025-04-14"] });
string fileName = $"presentation_{file.FileId.Substring(0, 8)}.pptx";
await using FileStream fileStream = File.Create(fileName);
Stream contentStream = await fileResponse.ReadAsStream();
await contentStream.CopyToAsync(fileStream);
}
```
@@ -29,6 +29,7 @@ To use Anthropic with Azure Foundry, you can check the sample [AgentProviders/Ag
|[Running a simple agent](./Agent_Anthropic_Step01_Running/)|This sample demonstrates how to create and run a basic agent with Anthropic Claude|
|[Using reasoning with an agent](./Agent_Anthropic_Step02_Reasoning/)|This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents|
|[Using function tools with an agent](./Agent_Anthropic_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with an Anthropic Claude agent|
|[Using Skills with an agent](./Agent_Anthropic_Step04_UsingSkills/)|This sample demonstrates how to use Anthropic-managed Skills (e.g., pptx) with an Anthropic Claude agent|
## Running the samples from the console
@@ -55,7 +55,7 @@ await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
Console.WriteLine("\n>> Serialize and deserialize the session to demonstrate persisted state\n");
JsonElement serializedSession = session.Serialize();
JsonElement serializedSession = agent.SerializeSession(session);
AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSession);
Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
@@ -47,7 +47,7 @@ Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", session));
Console.WriteLine(await agent.RunAsync("I am 20 years old", session));
// We can serialize the session. The serialized state will include the state of the memory component.
var sesionElement = session.Serialize();
JsonElement sesionElement = agent.SerializeSession(session);
Console.WriteLine("\n>> Use deserialized session with previously created memories\n");
@@ -29,36 +29,34 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
// Call the agent and check if there are any user input requests to handle.
// Call the agent and check if there are any function approval requests to handle.
// For simplicity, we are assuming here that only function approvals are pending.
AgentSession session = await agent.CreateSessionAsync();
var response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
var userInputRequests = response.UserInputRequests.ToList();
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
// For streaming use:
// var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", session).ToListAsync();
// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<FunctionApprovalRequestContent>().ToList();
while (userInputRequests.Count > 0)
while (approvalRequests.Count > 0)
{
// Ask the user to approve each function call request.
// For simplicity, we are assuming here that only function approval requests are being made.
var userInputResponses = userInputRequests
.OfType<FunctionApprovalRequestContent>()
.Select(functionApprovalRequest =>
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
})
.ToList();
});
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputResponses, session);
userInputRequests = response.UserInputRequests.ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
// For streaming use:
// updates = await agent.RunStreamingAsync(userInputResponses, session).ToListAsync();
// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<FunctionApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -25,7 +25,7 @@ AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// Serialize the session state to a JsonElement, so it can be stored for later use.
JsonElement serializedSession = session.Serialize();
JsonElement serializedSession = agent.SerializeSession(session);
// Save the serialized session to a temporary file (for demonstration purposes).
string tempFilePath = Path.GetTempFileName();
@@ -2,7 +2,9 @@
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
// This sample shows how to create and use a simple AI agent with a conversation that can be persisted to disk.
// This sample shows how to create and use a simple AI agent with custom ChatHistoryProvider that stores chat history in a custom storage location.
// The state of the custom ChatHistoryProvider (SessionDbKey) is stored with the agent session, so that when the session is resumed later,
// the chat history can be retrieved from the custom storage location.
using System.Text.Json;
using Azure.AI.OpenAI;
@@ -47,7 +49,7 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session
// Serialize the session state, so it can be stored for later use.
// Since the chat history is stored in the vector store, the serialized session
// only contains the guid that the messages are stored under in the vector store.
JsonElement serializedSession = session.Serialize();
JsonElement serializedSession = agent.SerializeSession(session);
Console.WriteLine("\n--- Serialized session ---\n");
Console.WriteLine(JsonSerializer.Serialize(serializedSession, new JsonSerializerOptions { WriteIndented = true }));
@@ -40,7 +40,7 @@ AgentResponse response = await agent.RunAsync("Write a very long novel about a t
// Poll for background responses until complete.
while (response.ContinuationToken is not null)
{
PersistAgentState(session, response.ContinuationToken);
PersistAgentState(agent, session, response.ContinuationToken);
await Task.Delay(TimeSpan.FromSeconds(10));
@@ -52,9 +52,9 @@ while (response.ContinuationToken is not null)
Console.WriteLine(response.Text);
void PersistAgentState(AgentSession? session, ResponseContinuationToken? continuationToken)
void PersistAgentState(AIAgent agent, AgentSession? session, ResponseContinuationToken? continuationToken)
{
stateStore["session"] = session!.Serialize();
stateStore["session"] = agent.SerializeSession(session!);
stateStore["continuationToken"] = JsonSerializer.SerializeToElement(continuationToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
}
@@ -210,28 +210,25 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
// This middleware handles Human in the loop console interaction for any user approval required during function calling.
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
var response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
var userInputRequests = response.UserInputRequests.ToList();
// For simplicity, we are assuming here that only function approvals are pending.
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
while (userInputRequests.Count > 0)
while (approvalRequests.Count > 0)
{
// Ask the user to approve each function call request.
// For simplicity, we are assuming here that only function approval requests are being made.
// Pass the user input responses back to the agent for further processing.
response.Messages = userInputRequests
.OfType<FunctionApprovalRequestContent>()
.Select(functionApprovalRequest =>
response.Messages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
})
.ToList();
});
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
userInputRequests = response.UserInputRequests.ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
}
return response;
@@ -65,7 +65,7 @@ Console.WriteLine(await agent.RunAsync("I need to make a dentist appointment for
Console.WriteLine(await agent.RunAsync("I've taken Sally to soccer practice.", session) + "\n");
// We can serialize the session, and it will contain both the chat history and the data that each AI context provider serialized.
JsonElement serializedSession = session.Serialize();
JsonElement serializedSession = agent.SerializeSession(session);
// Let's print it to console to show the contents.
Console.WriteLine(JsonSerializer.Serialize(serializedSession, options: new JsonSerializerOptions() { WriteIndented = true, IndentSize = 2 }) + "\n");
// The serialized session can be stored long term in a persistent store, but in this case we will just deserialize again and continue the conversation.
@@ -33,7 +33,7 @@ Before you begin, ensure you have the following prerequisites:
|[Using function tools with approvals](./Agent_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution|
|[Structured output with a simple agent](./Agent_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a simple agent|
|[Persisted conversations with a simple agent](./Agent_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later. This is useful for cases where an agent is hosted in a stateless service|
|[3rd party thread storage with a simple agent](./Agent_Step07_3rdPartyThreadStorage/)|This sample demonstrates how to store conversation history in a 3rd party storage solution|
|[3rd party chat history storage with a simple agent](./Agent_Step07_3rdPartyChatHistoryStorage/)|This sample demonstrates how to store chat history in a 3rd party storage solution|
|[Observability with a simple agent](./Agent_Step08_Observability/)|This sample demonstrates how to add telemetry to a simple agent|
|[Dependency injection with a simple agent](./Agent_Step09_DependencyInjection/)|This sample demonstrates how to add and resolve an agent with a dependency injection container|
|[Exposing a simple agent as MCP tool](./Agent_Step10_AsMcpTool/)|This sample demonstrates how to expose an agent as an MCP tool|
@@ -35,27 +35,25 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mo
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
// Check if there are any user input requests (approvals needed).
List<UserInputRequestContent> userInputRequests = response.UserInputRequests.ToList();
// Check if there are any approval requests.
// For simplicity, we are assuming here that only function approvals are pending.
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
while (userInputRequests.Count > 0)
while (approvalRequests.Count > 0)
{
// Ask the user to approve each function call request.
// For simplicity, we are assuming here that only function approval requests are being made.
List<ChatMessage> userInputMessages = userInputRequests
.OfType<FunctionApprovalRequestContent>()
.Select(functionApprovalRequest =>
List<ChatMessage> userInputMessages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
bool approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
})
.ToList();
});
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputMessages, session);
userInputRequests = response.UserInputRequests.ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -25,7 +25,7 @@ AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// Serialize the session state to a JsonElement, so it can be stored for later use.
JsonElement serializedSession = session.Serialize();
JsonElement serializedSession = agent.SerializeSession(session);
// Save the serialized session to a temporary file (for demonstration purposes).
string tempFilePath = Path.GetTempFileName();
@@ -193,27 +193,24 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
{
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
List<UserInputRequestContent> userInputRequests = response.UserInputRequests.ToList();
// For simplicity, we are assuming here that only function approvals are pending.
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
while (userInputRequests.Count > 0)
while (approvalRequests.Count > 0)
{
// Ask the user to approve each function call request.
// For simplicity, we are assuming here that only function approval requests are being made.
// Pass the user input responses back to the agent for further processing.
response.Messages = userInputRequests
.OfType<FunctionApprovalRequestContent>()
.Select(functionApprovalRequest =>
response.Messages = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
bool approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
})
.ToList();
});
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
userInputRequests = response.UserInputRequests.ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
}
return response;
@@ -75,17 +75,16 @@ AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAs
});
// You can then invoke the agent like any other AIAgent.
var sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
var userInputRequests = response.UserInputRequests.ToList();
// For simplicity, we are assuming here that only mcp tool approvals are pending.
AgentSession sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
AgentResponse response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
List<McpServerToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
while (userInputRequests.Count > 0)
while (approvalRequests.Count > 0)
{
// Ask the user to approve each MCP call request.
// For simplicity, we are assuming here that only MCP approval requests are being made.
var userInputResponses = userInputRequests
.OfType<McpServerToolApprovalRequestContent>()
.Select(approvalRequest =>
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(approvalRequest =>
{
Console.WriteLine($"""
The agent would like to invoke the following MCP Tool, please reply Y to approve.
@@ -94,13 +93,12 @@ while (userInputRequests.Count > 0)
Arguments: {string.Join(", ", approvalRequest.ToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
""");
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
})
.ToList();
});
// Pass the user input responses back to the agent for further processing.
response = await agentWithRequiredApproval.RunAsync(userInputResponses, sessionWithRequiredApproval);
userInputRequests = response.UserInputRequests.ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -64,17 +64,16 @@ AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
tools: [mcpToolWithApproval]);
// You can then invoke the agent like any other AIAgent.
var sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
var userInputRequests = response.UserInputRequests.ToList();
// For simplicity, we are assuming here that only mcp tool approvals are pending.
AgentSession sessionWithRequiredApproval = await agentWithRequiredApproval.CreateSessionAsync();
AgentResponse response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", sessionWithRequiredApproval);
List<McpServerToolApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
while (userInputRequests.Count > 0)
while (approvalRequests.Count > 0)
{
// Ask the user to approve each MCP call request.
// For simplicity, we are assuming here that only MCP approval requests are being made.
var userInputResponses = userInputRequests
.OfType<McpServerToolApprovalRequestContent>()
.Select(approvalRequest =>
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(approvalRequest =>
{
Console.WriteLine($"""
The agent would like to invoke the following MCP Tool, please reply Y to approve.
@@ -83,13 +82,12 @@ while (userInputRequests.Count > 0)
Arguments: {string.Join(", ", approvalRequest.ToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
""");
return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
})
.ToList();
});
// Pass the user input responses back to the agent for further processing.
response = await agentWithRequiredApproval.RunAsync(userInputResponses, sessionWithRequiredApproval);
userInputRequests = response.UserInputRequests.ToList();
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<McpServerToolApprovalRequestContent>().ToList();
}
Console.WriteLine($"\nAgent: {response}");
@@ -72,8 +72,8 @@ public static class Program
/// <summary>
/// Executor that starts the concurrent processing by sending messages to the agents.
/// </summary>
internal sealed class ConcurrentStartExecutor() :
Executor<string>("ConcurrentStartExecutor")
internal sealed partial class ConcurrentStartExecutor() :
Executor("ConcurrentStartExecutor")
{
/// <summary>
/// Starts the concurrent processing by sending messages to the agents.
@@ -83,7 +83,8 @@ internal sealed class ConcurrentStartExecutor() :
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the asynchronous operation</returns>
public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
[MessageHandler]
public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Broadcast the message to all connected agents. Receiving agents will queue
// the message but will not start processing until they receive a turn token.
@@ -0,0 +1,13 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<!-- Include Workflows source generator for samples using [MessageHandler] attribute -->
<ItemGroup>
<ProjectReference Include="$(RepoRoot)/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
GlobalPropertiesToRemove="TargetFramework" />
</ItemGroup>
</Project>
+41 -45
View File
@@ -80,7 +80,7 @@ internal sealed class AFAgentApplication : AgentApplication
}
// Serialize and save the updated conversation history back to turn state.
JsonElement sessionElementEnd = agentSession.Serialize(JsonUtilities.DefaultOptions);
JsonElement sessionElementEnd = this._agent.SerializeSession(agentSession, JsonUtilities.DefaultOptions);
turnState.SetValue("conversation.chatHistory", sessionElementEnd);
// End the streaming response
@@ -131,58 +131,54 @@ internal sealed class AFAgentApplication : AgentApplication
}
/// <summary>
/// When the agent returns any user input requests, this method converts them into adaptive cards that
/// When the agent returns any function approval requests, this method converts them into adaptive cards that
/// asks the user to approve or deny the requests.
/// </summary>
/// <param name="response">The <see cref="AgentResponse"/> that may contain the user input requests.</param>
/// <param name="response">The <see cref="AgentResponse"/> that may contain the function approval requests.</param>
/// <param name="attachments">The list of <see cref="Attachment"/> to which the adaptive cards will be added.</param>
private static void HandleUserInputRequests(AgentResponse response, ref List<Attachment>? attachments)
{
var userInputRequests = response.UserInputRequests.ToList();
if (userInputRequests.Count > 0)
foreach (FunctionApprovalRequestContent functionApprovalRequest in response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>())
{
foreach (var functionApprovalRequest in userInputRequests.OfType<FunctionApprovalRequestContent>())
var functionApprovalRequestJson = JsonSerializer.Serialize(functionApprovalRequest, JsonUtilities.DefaultOptions);
var card = new AdaptiveCard("1.5");
card.Body.Add(new AdaptiveTextBlock
{
var functionApprovalRequestJson = JsonSerializer.Serialize(functionApprovalRequest, JsonUtilities.DefaultOptions);
Text = "Function Call Approval Required",
Size = AdaptiveTextSize.Large,
Weight = AdaptiveTextWeight.Bolder,
HorizontalAlignment = AdaptiveHorizontalAlignment.Center
});
card.Body.Add(new AdaptiveTextBlock
{
Text = $"Function: {functionApprovalRequest.FunctionCall.Name}"
});
card.Body.Add(new AdaptiveActionSet()
{
Actions =
[
new AdaptiveSubmitAction
{
Id = "Approve",
Title = "Approve",
Data = new { type = "functionApproval", approved = true, requestJson = functionApprovalRequestJson }
},
new AdaptiveSubmitAction
{
Id = "Deny",
Title = "Deny",
Data = new { type = "functionApproval", approved = false, requestJson = functionApprovalRequestJson }
}
]
});
var card = new AdaptiveCard("1.5");
card.Body.Add(new AdaptiveTextBlock
{
Text = "Function Call Approval Required",
Size = AdaptiveTextSize.Large,
Weight = AdaptiveTextWeight.Bolder,
HorizontalAlignment = AdaptiveHorizontalAlignment.Center
});
card.Body.Add(new AdaptiveTextBlock
{
Text = $"Function: {functionApprovalRequest.FunctionCall.Name}"
});
card.Body.Add(new AdaptiveActionSet()
{
Actions =
[
new AdaptiveSubmitAction
{
Id = "Approve",
Title = "Approve",
Data = new { type = "functionApproval", approved = true, requestJson = functionApprovalRequestJson }
},
new AdaptiveSubmitAction
{
Id = "Deny",
Title = "Deny",
Data = new { type = "functionApproval", approved = false, requestJson = functionApprovalRequestJson }
}
]
});
attachments ??= [];
attachments.Add(new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = card.ToJson(),
});
}
attachments ??= [];
attachments.Add(new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = card.ToJson(),
});
}
}
}
+16 -3
View File
@@ -54,7 +54,7 @@ public sealed class A2AAgent : AIAgent
}
/// <inheritdoc/>
public sealed override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected sealed override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new A2AAgentSession());
/// <summary>
@@ -66,8 +66,21 @@ public sealed class A2AAgent : AIAgent
=> new(new A2AAgentSession() { ContextId = contextId });
/// <inheritdoc/>
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new A2AAgentSession(serializedSession, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
_ = Throw.IfNull(session);
if (session is not A2AAgentSession typedSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return typedSession.Serialize(jsonSerializerOptions);
}
/// <inheritdoc/>
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new A2AAgentSession(serializedState, jsonSerializerOptions));
/// <inheritdoc/>
protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
@@ -46,7 +46,7 @@ public sealed class A2AAgentSession : AgentSession
public string? TaskId { get; internal set; }
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
internal JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var state = new A2AAgentSessionState
{
@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -22,6 +24,8 @@ namespace Microsoft.Agents.AI;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract partial class AIAgent
{
private static readonly AsyncLocal<AgentRunContext?> s_currentContext = new();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay =>
this.Name is { } name ? $"Id = {this.Id}, Name = {name}" : $"Id = {this.Id}";
@@ -76,6 +80,18 @@ public abstract partial class AIAgent
/// </remarks>
public virtual string? Description { get; }
/// <summary>
/// Gets or sets the <see cref="AgentRunContext"/> for the current agent run.
/// </summary>
/// <remarks>
/// This value flows across async calls.
/// </remarks>
public static AgentRunContext? CurrentRunContext
{
get => s_currentContext.Value;
protected set => s_currentContext.Value = value;
}
/// <summary>Asks the <see cref="AIAgent"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
@@ -123,23 +139,74 @@ public abstract partial class AIAgent
/// may be deferred until first use to optimize performance.
/// </para>
/// </remarks>
public abstract ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default);
public ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
=> this.CreateSessionCoreAsync(cancellationToken);
/// <summary>
/// Core implementation of session creation logic.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance ready for use with this agent.</returns>
/// <remarks>
/// This is the primary session creation method that implementations must override.
/// </remarks>
protected abstract ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Serializes an agent session to its JSON representation.
/// </summary>
/// <param name="session">The <see cref="AgentSession"/> to serialize.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the serialization process.</param>
/// <returns>A <see cref="JsonElement"/> containing the serialized session state.</returns>
/// <exception cref="ArgumentNullException"><paramref name="session"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The type of <paramref name="session"/> is not supported by this agent.</exception>
/// <remarks>
/// This method enables saving conversation sessions to persistent storage,
/// allowing conversations to resume across application restarts or be migrated between
/// different agent instances. Use <see cref="DeserializeSessionAsync"/> to restore the session.
/// </remarks>
public JsonElement SerializeSession(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> this.SerializeSessionCore(session, jsonSerializerOptions);
/// <summary>
/// Core implementation of session serialization logic.
/// </summary>
/// <param name="session">The <see cref="AgentSession"/> to serialize.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the serialization process.</param>
/// <returns>A <see cref="JsonElement"/> containing the serialized session state.</returns>
/// <remarks>
/// This is the primary session serialization method that implementations must override.
/// </remarks>
protected abstract JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null);
/// <summary>
/// Deserializes an agent session from its JSON serialized representation.
/// </summary>
/// <param name="serializedSession">A <see cref="JsonElement"/> containing the serialized session state.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> containing the serialized session state.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedSession"/>.</returns>
/// <exception cref="ArgumentException">The <paramref name="serializedSession"/> is not in the expected format.</exception>
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedState"/>.</returns>
/// <exception cref="ArgumentException">The <paramref name="serializedState"/> is not in the expected format.</exception>
/// <exception cref="JsonException">The serialized data is invalid or cannot be deserialized.</exception>
/// <remarks>
/// This method enables restoration of conversation sessions from previously saved state,
/// allowing conversations to resume across application restarts or be migrated between
/// different agent instances.
/// </remarks>
public abstract ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
public ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.DeserializeSessionCoreAsync(serializedState, jsonSerializerOptions, cancellationToken);
/// <summary>
/// Core implementation of session deserialization logic.
/// </summary>
/// <param name="serializedState">A <see cref="JsonElement"/> containing the serialized session state.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedState"/>.</returns>
/// <remarks>
/// This is the primary session deserialization method that implementations must override.
/// </remarks>
protected abstract ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the session.
@@ -237,8 +304,11 @@ public abstract partial class AIAgent
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreAsync(messages, session, options, cancellationToken);
CancellationToken cancellationToken = default)
{
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
return this.RunCoreAsync(messages, session, options, cancellationToken);
}
/// <summary>
/// Core implementation of the agent invocation logic with a collection of chat messages.
@@ -355,12 +425,22 @@ public abstract partial class AIAgent
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
/// </para>
/// </remarks>
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
public async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreStreamingAsync(messages, session, options, cancellationToken);
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
AgentRunContext context = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
CurrentRunContext = context;
await foreach (var update in this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
// Restore context again when resuming after the caller code executes.
CurrentRunContext = context;
}
}
/// <summary>
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
@@ -129,13 +129,30 @@ public abstract class AIContextProvider
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The messages to be used by the agent for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokingContext(IEnumerable<ChatMessage> requestMessages)
public InvokingContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages)
{
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the caller provided messages that will be used by the agent for this invocation.
/// </summary>
@@ -158,15 +175,33 @@ public abstract class AIContextProvider
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <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(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? aiContextProviderMessages)
{
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.AIContextProviderMessages = aiContextProviderMessages;
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the caller provided messages that were used by the agent for this invocation.
/// </summary>
@@ -3,7 +3,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
#if NET
using System.Text;
#endif
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -139,21 +143,6 @@ public class AgentResponse
[JsonIgnore]
public string Text => this._messages?.ConcatText() ?? string.Empty;
/// <summary>
/// Gets all user input requests present in the response messages.
/// </summary>
/// <value>
/// An enumerable collection of <see cref="UserInputRequestContent"/> instances found
/// across all messages in the response.
/// </value>
/// <remarks>
/// User input requests indicate that the agent is asking for additional information
/// from the user before it can continue processing. This property aggregates all such
/// requests across all messages in the response.
/// </remarks>
[JsonIgnore]
public IEnumerable<UserInputRequestContent> UserInputRequests => this._messages?.SelectMany(x => x.Contents).OfType<UserInputRequestContent>() ?? [];
/// <summary>
/// Gets or sets the identifier of the agent that generated this response.
/// </summary>
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -95,13 +94,6 @@ public class AgentResponseUpdate
[JsonIgnore]
public string Text => this._contents is not null ? this._contents.ConcatText() : string.Empty;
/// <summary>Gets the user input requests associated with the response.</summary>
/// <remarks>
/// This property concatenates all <see cref="UserInputRequestContent"/> instances in the response.
/// </remarks>
[JsonIgnore]
public IEnumerable<UserInputRequestContent> UserInputRequests => this._contents?.OfType<UserInputRequestContent>() ?? [];
/// <summary>Gets or sets the agent run response update content items.</summary>
[AllowNull]
public IList<AIContent> Contents
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>Provides context for an in-flight agent run.</summary>
public sealed class AgentRunContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentRunContext"/> class.
/// </summary>
/// <param name="agent">The <see cref="AIAgent"/> that is executing the current run.</param>
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run if any.</param>
/// <param name="requestMessages">The request messages passed into the current run.</param>
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run.</param>
public AgentRunContext(
AIAgent agent,
AgentSession? session,
IReadOnlyCollection<ChatMessage> requestMessages,
AgentRunOptions? agentRunOptions)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.RunOptions = agentRunOptions;
}
/// <summary>Gets the <see cref="AIAgent"/> that is executing the current run.</summary>
public AIAgent Agent { get; }
/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
public AgentSession? Session { get; }
/// <summary>Gets the request messages passed into the current run.</summary>
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
public AgentRunOptions? RunOptions { get; }
}
@@ -36,7 +36,7 @@ namespace Microsoft.Agents.AI;
/// <para>
/// To support conversations that may need to survive application restarts or separate service requests, an <see cref="AgentSession"/> can be serialized
/// and deserialized, so that it can be saved in a persistent store.
/// The <see cref="AgentSession"/> provides the <see cref="Serialize(JsonSerializerOptions?)"/> method to serialize the session to a
/// The <see cref="AIAgent"/> provides the <see cref="AIAgent.SerializeSession(AgentSession, JsonSerializerOptions?)"/> method to serialize the session to a
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> method
/// can be used to deserialize the session.
/// </para>
@@ -53,14 +53,6 @@ public abstract class AgentSession
{
}
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
/// <summary>Asks the <see cref="AgentSession"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
@@ -143,13 +143,30 @@ public abstract class ChatHistoryProvider
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The new messages to be used by the agent for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokingContext(IEnumerable<ChatMessage> requestMessages)
public InvokingContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages)
{
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the caller provided messages that will be used by the agent for this invocation.
/// </summary>
@@ -172,15 +189,33 @@ public abstract class ChatHistoryProvider
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
/// </summary>
/// <param name="agent">The agent being invoked.</param>
/// <param name="session">The session associated with the agent invocation.</param>
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
/// <param name="chatHistoryProviderMessages">The messages retrieved from the <see cref="ChatHistoryProvider"/> for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage>? chatHistoryProviderMessages)
public InvokedContext(
AIAgent agent,
AgentSession? session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages)
{
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.ChatHistoryProviderMessages = chatHistoryProviderMessages;
}
/// <summary>
/// Gets the agent that is being invoked.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets the agent session associated with the agent invocation.
/// </summary>
public AgentSession? Session { get; }
/// <summary>
/// Gets the caller provided messages that were used by the agent for this invocation.
/// </summary>
@@ -74,11 +74,15 @@ public abstract class DelegatingAIAgent : AIAgent
}
/// <inheritdoc />
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) => this.InnerAgent.CreateSessionAsync(cancellationToken);
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) => this.InnerAgent.CreateSessionAsync(cancellationToken);
/// <inheritdoc />
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.DeserializeSessionAsync(serializedSession, jsonSerializerOptions, cancellationToken);
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> this.InnerAgent.SerializeSession(session, jsonSerializerOptions);
/// <inheritdoc />
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.DeserializeSessionAsync(serializedState, jsonSerializerOptions, cancellationToken);
/// <inheritdoc />
protected override Task<AgentResponse> RunCoreAsync(
@@ -58,29 +58,29 @@ public abstract class InMemoryAgentSession : AgentSession
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryAgentSession"/> class from previously serialized state.
/// </summary>
/// <param name="serializedSessionState">A <see cref="JsonElement"/> representing the serialized state of the session.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the session.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="chatHistoryProviderFactory">
/// Optional factory function to create the <see cref="InMemoryChatHistoryProvider"/> from its serialized state.
/// If not provided, a default factory will be used that creates a basic <see cref="InMemoryChatHistoryProvider"/>.
/// </param>
/// <exception cref="ArgumentException">The <paramref name="serializedSessionState"/> is not a JSON object.</exception>
/// <exception cref="JsonException">The <paramref name="serializedSessionState"/> is invalid or cannot be deserialized to the expected type.</exception>
/// <exception cref="ArgumentException">The <paramref name="serializedState"/> is not a JSON object.</exception>
/// <exception cref="JsonException">The <paramref name="serializedState"/> is invalid or cannot be deserialized to the expected type.</exception>
/// <remarks>
/// This constructor enables restoration of in-memory threads from previously saved state, allowing
/// conversations to be resumed across application restarts or migrated between different instances.
/// </remarks>
protected InMemoryAgentSession(
JsonElement serializedSessionState,
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, InMemoryChatHistoryProvider>? chatHistoryProviderFactory = null)
{
if (serializedSessionState.ValueKind != JsonValueKind.Object)
if (serializedState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedSessionState));
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedState));
}
var state = serializedSessionState.Deserialize(
var state = serializedState.Deserialize(
AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(InMemoryAgentSessionState))) as InMemoryAgentSessionState;
this.ChatHistoryProvider =
@@ -98,7 +98,7 @@ public abstract class InMemoryAgentSession : AgentSession
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
protected internal virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var chatHistoryProviderState = this.ChatHistoryProvider.Serialize(jsonSerializerOptions);
@@ -42,24 +42,24 @@ public abstract class ServiceIdAgentSession : AgentSession
/// <summary>
/// Initializes a new instance of the <see cref="ServiceIdAgentSession"/> class from previously serialized state.
/// </summary>
/// <param name="serializedSessionState">A <see cref="JsonElement"/> representing the serialized state of the session.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the session.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <exception cref="ArgumentException">The <paramref name="serializedSessionState"/> is not a JSON object.</exception>
/// <exception cref="JsonException">The <paramref name="serializedSessionState"/> is invalid or cannot be deserialized to the expected type.</exception>
/// <exception cref="ArgumentException">The <paramref name="serializedState"/> is not a JSON object.</exception>
/// <exception cref="JsonException">The <paramref name="serializedState"/> is invalid or cannot be deserialized to the expected type.</exception>
/// <remarks>
/// This constructor enables restoration of a service-backed session from serialized state, typically used
/// when deserializing session information that was previously saved or transmitted across application boundaries.
/// </remarks>
protected ServiceIdAgentSession(
JsonElement serializedSessionState,
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null)
{
if (serializedSessionState.ValueKind != JsonValueKind.Object)
if (serializedState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedSessionState));
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedState));
}
var state = serializedSessionState.Deserialize(
var state = serializedState.Deserialize(
AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ServiceIdAgentSessionState))) as ServiceIdAgentSessionState;
if (state?.ServiceSessionId is string serviceSessionId)
@@ -85,13 +85,9 @@ public abstract class ServiceIdAgentSession : AgentSession
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use for the serialization process.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state, containing the service session identifier.</returns>
/// <remarks>
/// The serialized state contains only the service session identifier, as all other conversation state
/// is maintained remotely by the backing service. This makes the serialized representation very lightweight.
/// </remarks>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
protected internal virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var state = new ServiceIdAgentSessionState
{
@@ -64,13 +64,27 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion agentVersion, ChatOptions? chatOptions)
: this(
aiProjectClient,
new AgentReference(Throw.IfNull(agentVersion).Name, agentVersion.Version),
CreateAgentReference(Throw.IfNull(agentVersion)),
(agentVersion.Definition as PromptAgentDefinition)?.Model,
chatOptions)
{
this._agentVersion = agentVersion;
}
/// <summary>
/// Creates an <see cref="AgentReference"/> from an <see cref="AgentVersion"/>.
/// Uses the agent version's version if available, otherwise defaults to "latest".
/// </summary>
/// <param name="agentVersion">The agent version to create a reference from.</param>
/// <returns>An <see cref="AgentReference"/> for the specified agent version.</returns>
private static AgentReference CreateAgentReference(AgentVersion agentVersion)
{
// If the version is null, empty, or whitespace, use "latest" as the default.
// This handles cases where hosted agents (like MCP agents) may not have a version assigned.
var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version;
return new AgentReference(agentVersion.Name, version);
}
/// <inheritdoc/>
public override object? GetService(Type serviceType, object? serviceKey = null)
{
@@ -543,9 +543,16 @@ public static partial class AzureAIProjectChatClientExtensions
}
}
// Use the agent version's ID if available, otherwise generate one from name and version.
// This handles cases where hosted agents (like MCP agents) may not have an ID assigned.
var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version;
var agentId = string.IsNullOrWhiteSpace(agentVersion.Id)
? $"{agentVersion.Name}:{version}"
: agentVersion.Id;
var agentOptions = new ChatClientAgentOptions()
{
Id = agentVersion.Id,
Id = agentId,
Name = agentVersion.Name,
Description = agentVersion.Description,
};
@@ -42,7 +42,7 @@ public class CopilotStudioAgent : AIAgent
}
/// <inheritdoc/>
public sealed override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected sealed override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new CopilotStudioAgentSession());
/// <summary>
@@ -54,8 +54,21 @@ public class CopilotStudioAgent : AIAgent
=> new(new CopilotStudioAgentSession() { ConversationId = conversationId });
/// <inheritdoc/>
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CopilotStudioAgentSession(serializedSession, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
Throw.IfNull(session);
if (session is not CopilotStudioAgentSession typedSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return typedSession.Serialize(jsonSerializerOptions);
}
/// <inheritdoc/>
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CopilotStudioAgentSession(serializedState, jsonSerializerOptions));
/// <inheritdoc/>
protected override async Task<AgentResponse> RunCoreAsync(
@@ -25,4 +25,12 @@ public sealed class CopilotStudioAgentSession : ServiceIdAgentSession
get { return this.ServiceSessionId; }
internal set { this.ServiceSessionId = value; }
}
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
internal new JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> base.Serialize(jsonSerializerOptions);
}
@@ -8,6 +8,9 @@
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
- Removed AgentThreadMetadata and used AgentSessionId directly instead ([#3067](https://github.com/microsoft/agent-framework/pull/3067));
- Renamed AgentThread to AgentSession ([#3430](https://github.com/microsoft/agent-framework/pull/3430))
- Moved AgentSession.Serialize to AIAgent.SerializeSession ([#3650](https://github.com/microsoft/agent-framework/pull/3650))
- Renamed serializedSession parameter to serializedState on DeserializeSessionAsync for consistency ([#3681](https://github.com/microsoft/agent-framework/pull/3681))
- Introduce Core method pattern for Session management methods on AIAgent ([#3699](https://github.com/microsoft/agent-framework/pull/3699))
- Updated to use base `AgentRunOptions.ResponseFormat` for structured output configuration ([#3658](https://github.com/microsoft/agent-framework/pull/3658))
## v1.0.0-preview.251204.1
@@ -32,24 +32,45 @@ public sealed class DurableAIAgent : AIAgent
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a new agent session.</returns>
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
{
AgentSessionId sessionId = this._context.NewAgentSessionId(this._agentName);
return ValueTask.FromResult<AgentSession>(new DurableAgentSession(sessionId));
}
/// <summary>
/// Serializes an agent session to JSON.
/// </summary>
/// <param name="session">The session to serialize.</param>
/// <param name="jsonSerializerOptions">Optional JSON serializer options.</param>
/// <returns>A <see cref="JsonElement"/> containing the serialized session state.</returns>
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
if (session is not DurableAgentSession durableSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return durableSession.Serialize(jsonSerializerOptions);
}
/// <summary>
/// Deserializes an agent session from JSON.
/// </summary>
/// <param name="serializedSession">The serialized session data.</param>
/// <param name="serializedState">The serialized session data.</param>
/// <param name="jsonSerializerOptions">Optional JSON serializer options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains the deserialized agent session.</returns>
public override ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedSession,
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult<AgentSession>(DurableAgentSession.Deserialize(serializedSession, jsonSerializerOptions));
return ValueTask.FromResult<AgentSession>(DurableAgentSession.Deserialize(serializedState, jsonSerializerOptions));
}
/// <summary>
@@ -11,14 +11,29 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
public override string? Name { get; } = name;
public override ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedSession,
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
return ValueTask.FromResult<AgentSession>(DurableAgentSession.Deserialize(serializedSession, jsonSerializerOptions));
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
if (session is not DurableAgentSession durableSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return durableSession.Serialize(jsonSerializerOptions);
}
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult<AgentSession>(DurableAgentSession.Deserialize(serializedState, jsonSerializerOptions));
}
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
{
return ValueTask.FromResult<AgentSession>(new DurableAgentSession(AgentSessionId.WithRandomKey(this.Name!)));
}
@@ -26,7 +26,7 @@ public sealed class DurableAgentSession : AgentSession
internal AgentSessionId SessionId { get; }
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
internal JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
return JsonSerializer.SerializeToElement(
this,
@@ -86,7 +86,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
}
/// <inheritdoc/>
public sealed override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected sealed override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new GitHubCopilotAgentSession());
/// <summary>
@@ -98,11 +98,24 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
=> new(new GitHubCopilotAgentSession() { SessionId = sessionId });
/// <inheritdoc/>
public override ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedSession,
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
_ = Throw.IfNull(session);
if (session is not GitHubCopilotAgentSession typedSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return typedSession.Serialize(jsonSerializerOptions);
}
/// <inheritdoc/>
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(new GitHubCopilotAgentSession(serializedSession, jsonSerializerOptions));
=> new(new GitHubCopilotAgentSession(serializedState, jsonSerializerOptions));
/// <inheritdoc/>
protected override Task<AgentResponse> RunCoreAsync(
@@ -36,7 +36,7 @@ public sealed class GitHubCopilotAgentSession : AgentSession
}
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
internal JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
State state = new()
{
@@ -33,7 +33,7 @@ public sealed class InMemoryAgentSessionStore : AgentSessionStore
public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default)
{
var key = GetKey(conversationId, agent.Id);
this._threads[key] = session.Serialize();
this._threads[key] = agent.SerializeSession(session);
return default;
}
@@ -30,13 +30,19 @@ internal class PurviewAgent : AIAgent, IDisposable
}
/// <inheritdoc/>
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
return this._innerAgent.DeserializeSessionAsync(serializedSession, jsonSerializerOptions, cancellationToken);
return this._innerAgent.SerializeSession(session, jsonSerializerOptions);
}
/// <inheritdoc/>
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return this._innerAgent.DeserializeSessionAsync(serializedState, jsonSerializerOptions, cancellationToken);
}
/// <inheritdoc/>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
{
return this._innerAgent.CreateSessionAsync(cancellationToken);
}
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Microsoft.Agents.AI.Workflows.Generators.Models;
/// <summary>
/// A wrapper around <see cref="ImmutableArray{T}"/> that provides value-based equality.
/// This is necessary for incremental generator caching since ImmutableArray uses reference equality.
/// </summary>
/// <remarks>
/// Creates a new <see cref="EquatableArray{T}"/> from an <see cref="ImmutableArray{T}"/>.
/// </remarks>
internal readonly struct EquatableArray<T>(ImmutableArray<T> array) : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
private readonly ImmutableArray<T> _array = array.IsDefault ? ImmutableArray<T>.Empty : array;
/// <summary>
/// Gets the underlying array.
/// </summary>
public ImmutableArray<T> AsImmutableArray() => this._array;
/// <summary>
/// Gets the number of elements in the array.
/// </summary>
public int Length => this._array.Length;
/// <summary>
/// Gets the element at the specified index.
/// </summary>
public T this[int index] => this._array[index];
/// <summary>
/// Gets whether the array is empty.
/// </summary>
public bool IsEmpty => this._array.IsEmpty;
/// <inheritdoc/>
public bool Equals(EquatableArray<T> other)
{
if (this._array.Length != other._array.Length)
{
return false;
}
for (int i = 0; i < this._array.Length; i++)
{
if (!this._array[i].Equals(other._array[i]))
{
return false;
}
}
return true;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is EquatableArray<T> other && this.Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
if (this._array.IsEmpty)
{
return 0;
}
var hashCode = 17;
foreach (var item in this._array)
{
hashCode = hashCode * 31 + (item?.GetHashCode() ?? 0);
}
return hashCode;
}
/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)this._array).GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/// <summary>
/// Equality operator.
/// </summary>
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}
/// <summary>
/// Inequality operator.
/// </summary>
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
/// <summary>
/// Creates an empty <see cref="EquatableArray{T}"/>.
/// </summary>
public static EquatableArray<T> Empty => new(ImmutableArray<T>.Empty);
/// <summary>
/// Implicit conversion from <see cref="ImmutableArray{T}"/>.
/// </summary>
public static implicit operator EquatableArray<T>(ImmutableArray<T> array) => new(array);
}
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
/// <summary>
/// Declares that an executor may yield messages of the specified type as workflow outputs.
/// </summary>
/// <remarks>
/// <para>
/// Apply this attribute to an <see cref="Executor"/> class to declare the types of messages
/// it may yield via <see cref="IWorkflowContext.YieldOutputAsync"/>. This information is used
/// for protocol validation and documentation.
/// </para>
/// <para>
/// This attribute can be applied multiple times to declare multiple output types.
/// It is inherited by derived classes, allowing base executors to declare common output types.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// [YieldsMessage(typeof(FinalResult))]
/// [YieldsMessage(typeof(StreamChunk))]
/// public partial class MyExecutor : Executor
/// {
/// // ...
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class YieldsMessageAttribute : Attribute
{
/// <summary>
/// Gets the type of message that the executor may yield.
/// </summary>
public Type Type { get; }
/// <summary>
/// Initializes a new instance of the <see cref="YieldsMessageAttribute"/> class.
/// </summary>
/// <param name="type">The type of message that the executor may yield.</param>
/// <exception cref="ArgumentNullException"><paramref name="type"/> is <see langword="null"/>.</exception>
public YieldsMessageAttribute(Type type)
{
this.Type = Throw.IfNull(type);
}
}
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - Internal use of obsolete types for backward compatibility
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -9,6 +10,12 @@ namespace Microsoft.Agents.AI.Workflows.Reflection;
/// A message handler interface for handling messages of type <typeparamref name="TMessage"/>.
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <remarks>
/// This interface is obsolete. Use the <see cref="MessageHandlerAttribute"/> on methods in a partial class
/// deriving from <see cref="Executor"/> instead.
/// </remarks>
[Obsolete("Use [MessageHandler] attribute on methods in a partial class deriving from Executor. " +
"This interface will be removed in a future version.")]
public interface IMessageHandler<TMessage>
{
/// <summary>
@@ -28,6 +35,12 @@ public interface IMessageHandler<TMessage>
/// </summary>
/// <typeparam name="TMessage">The type of message to handle.</typeparam>
/// <typeparam name="TResult">The type of result returned after handling the message.</typeparam>
/// <remarks>
/// This interface is obsolete. Use the <see cref="MessageHandlerAttribute"/> on methods in a partial class
/// deriving from <see cref="Executor"/> instead.
/// </remarks>
[Obsolete("Use [MessageHandler] attribute on methods in a partial class deriving from Executor. " +
"This interface will be removed in a future version.")]
public interface IMessageHandler<TMessage, TResult>
{
/// <summary>
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - Internal use of obsolete types for backward compatibility
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Agents.AI.Workflows.Reflection;
@@ -10,6 +11,12 @@ namespace Microsoft.Agents.AI.Workflows.Reflection;
/// <typeparam name="TExecutor">The actual type of the <see cref="ReflectingExecutor{TExecutor}"/>.
/// This is used to reflectively discover handlers for messages without violating ILTrim requirements.
/// </typeparam>
/// <remarks>
/// This type is obsolete. Use the <see cref="MessageHandlerAttribute"/> on methods in a partial class
/// deriving from <see cref="Executor"/> instead.
/// </remarks>
[Obsolete("Use [MessageHandler] attribute on methods in a partial class deriving from Executor. " +
"This type will be removed in a future version.")]
public class ReflectingExecutor<
[DynamicallyAccessedMembers(
ReflectionDemands.RuntimeInterfaceDiscoveryAndInvocation)
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - Internal use of obsolete types for backward compatibility
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -101,7 +101,8 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
protected internal override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
AIAgentHostState state = new(this._session?.Serialize(), this._currentTurnEmitEvents);
JsonElement? sessionState = this._session is not null ? this._agent.SerializeSession(this._session) : null;
AIAgentHostState state = new(sessionState, this._currentTurnEmitEvents);
Task coreStateTask = context.QueueStateUpdateAsync(AIAgentHostStateKey, state, cancellationToken: cancellationToken).AsTask();
Task userInputRequestsTask = this._userInputHandler?.OnCheckpointingAsync(UserInputRequestStateKey, context, cancellationToken).AsTask() ?? Task.CompletedTask;
Task functionCallRequestsTask = this._functionCallHandler?.OnCheckpointingAsync(FunctionCallRequestStateKey, context, cancellationToken).AsTask() ?? Task.CompletedTask;
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - Internal use of obsolete types for backward compatibility
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -65,11 +65,23 @@ internal sealed class WorkflowHostAgent : AIAgent
protocol.ThrowIfNotChatProtocol(allowCatchAll: true);
}
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new WorkflowSession(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse));
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new WorkflowSession(this._workflow, serializedSession, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
_ = Throw.IfNull(session);
if (session is not WorkflowSession workflowSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return workflowSession.Serialize(jsonSerializerOptions);
}
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new WorkflowSession(this._workflow, serializedState, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse, jsonSerializerOptions));
private async ValueTask<WorkflowSession> UpdateSessionAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, CancellationToken cancellationToken = default)
{
@@ -75,7 +75,7 @@ internal sealed class WorkflowSession : AgentSession
public CheckpointInfo? LastCheckpoint { get; set; }
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
internal JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
JsonMarshaller marshaller = new(jsonSerializerOptions);
SessionState info = new(
@@ -231,8 +231,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -246,8 +246,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -273,8 +273,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
}
@@ -286,10 +286,10 @@ public sealed partial class ChatClientAgent : AIAgent
await this.UpdateSessionWithTypeAndConversationIdAsync(safeSession, chatResponse.ConversationId, cancellationToken).ConfigureAwait(false);
// To avoid inconsistent state we only notify the session of the input messages if no error occurs after the initial request.
await NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, GetInputMessages(inputMessages, continuationToken), chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
// Notify the AIContextProvider of all new messages.
await NotifyAIContextProviderOfSuccessAsync(safeSession, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfSuccessAsync(safeSession, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -302,7 +302,7 @@ public sealed partial class ChatClientAgent : AIAgent
: this.ChatClient.GetService(serviceType, serviceKey));
/// <inheritdoc/>
public override async ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override async ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
{
ChatHistoryProvider? chatHistoryProvider = this._agentOptions?.ChatHistoryProviderFactory is not null
? await this._agentOptions.ChatHistoryProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
@@ -386,7 +386,20 @@ public sealed partial class ChatClientAgent : AIAgent
}
/// <inheritdoc/>
public override async ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
_ = Throw.IfNull(session);
if (session is not ChatClientAgentSession typedSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return typedSession.Serialize(jsonSerializerOptions);
}
/// <inheritdoc/>
protected override async ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatHistoryProvider>>? chatHistoryProviderFactory = this._agentOptions?.ChatHistoryProviderFactory is null ?
null :
@@ -397,7 +410,7 @@ public sealed partial class ChatClientAgent : AIAgent
(jse, jso, ct) => this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso }, ct);
return await ChatClientAgentSession.DeserializeAsync(
serializedSession,
serializedState,
jsonSerializerOptions,
chatHistoryProviderFactory,
aiContextProviderFactory,
@@ -442,8 +455,8 @@ public sealed partial class ChatClientAgent : AIAgent
}
catch (Exception ex)
{
await NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await NotifyAIContextProviderOfFailureAsync(safeSession, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfFailureAsync(safeSession, ex, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfFailureAsync(safeSession, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -460,10 +473,10 @@ public sealed partial class ChatClientAgent : AIAgent
}
// Only notify the session of new messages if the chatResponse was successful to avoid inconsistent message state in the session.
await NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
await this.NotifyChatHistoryProviderOfNewMessagesAsync(safeSession, inputMessages, chatHistoryProviderMessages, aiContextProviderMessages, chatResponse.Messages, chatOptions, cancellationToken).ConfigureAwait(false);
// Notify the AIContextProvider of all new messages.
await NotifyAIContextProviderOfSuccessAsync(safeSession, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
await this.NotifyAIContextProviderOfSuccessAsync(safeSession, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
var agentResponse = agentResponseFactoryFunc(chatResponse);
@@ -475,7 +488,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// <summary>
/// Notify the <see cref="AIContextProvider"/> when an agent run succeeded, if there is an <see cref="AIContextProvider"/>.
/// </summary>
private static async Task NotifyAIContextProviderOfSuccessAsync(
private async Task NotifyAIContextProviderOfSuccessAsync(
ChatClientAgentSession session,
IEnumerable<ChatMessage> inputMessages,
IList<ChatMessage>? aiContextProviderMessages,
@@ -484,7 +497,7 @@ public sealed partial class ChatClientAgent : AIAgent
{
if (session.AIContextProvider is not null)
{
await session.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProviderMessages) { ResponseMessages = responseMessages },
await session.AIContextProvider.InvokedAsync(new(this, session, inputMessages, aiContextProviderMessages) { ResponseMessages = responseMessages },
cancellationToken).ConfigureAwait(false);
}
}
@@ -492,7 +505,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// <summary>
/// Notify the <see cref="AIContextProvider"/> of any failure during an agent run, if there is an <see cref="AIContextProvider"/>.
/// </summary>
private static async Task NotifyAIContextProviderOfFailureAsync(
private async Task NotifyAIContextProviderOfFailureAsync(
ChatClientAgentSession session,
Exception ex,
IEnumerable<ChatMessage> inputMessages,
@@ -501,7 +514,7 @@ public sealed partial class ChatClientAgent : AIAgent
{
if (session.AIContextProvider is not null)
{
await session.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProviderMessages) { InvokeException = ex },
await session.AIContextProvider.InvokedAsync(new(this, session, inputMessages, aiContextProviderMessages) { InvokeException = ex },
cancellationToken).ConfigureAwait(false);
}
}
@@ -719,7 +732,7 @@ public sealed partial class ChatClientAgent : AIAgent
// Add any existing messages from the session to the messages to be sent to the chat client.
if (chatHistoryProvider is not null)
{
var invokingContext = new ChatHistoryProvider.InvokingContext(inputMessages);
var invokingContext = new ChatHistoryProvider.InvokingContext(this, typedSession, inputMessages);
var providerMessages = await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
inputMessagesForChatClient.AddRange(providerMessages);
chatHistoryProviderMessages = providerMessages as IList<ChatMessage> ?? providerMessages.ToList();
@@ -732,7 +745,7 @@ public sealed partial class ChatClientAgent : AIAgent
// messages and options with the additional context.
if (typedSession.AIContextProvider is not null)
{
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
var invokingContext = new AIContextProvider.InvokingContext(this, typedSession, inputMessages);
var aiContext = await typedSession.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
if (aiContext.Messages is { Count: > 0 })
{
@@ -805,7 +818,7 @@ public sealed partial class ChatClientAgent : AIAgent
}
}
private static Task NotifyChatHistoryProviderOfFailureAsync(
private Task NotifyChatHistoryProviderOfFailureAsync(
ChatClientAgentSession session,
Exception ex,
IEnumerable<ChatMessage> requestMessages,
@@ -820,7 +833,7 @@ public sealed partial class ChatClientAgent : AIAgent
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
if (provider is not null)
{
var invokedContext = new ChatHistoryProvider.InvokedContext(requestMessages, chatHistoryProviderMessages!)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, requestMessages, chatHistoryProviderMessages!)
{
AIContextProviderMessages = aiContextProviderMessages,
InvokeException = ex
@@ -832,7 +845,7 @@ public sealed partial class ChatClientAgent : AIAgent
return Task.CompletedTask;
}
private static Task NotifyChatHistoryProviderOfNewMessagesAsync(
private Task NotifyChatHistoryProviderOfNewMessagesAsync(
ChatClientAgentSession session,
IEnumerable<ChatMessage> requestMessages,
IEnumerable<ChatMessage>? chatHistoryProviderMessages,
@@ -847,7 +860,7 @@ public sealed partial class ChatClientAgent : AIAgent
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
if (provider is not null)
{
var invokedContext = new ChatHistoryProvider.InvokedContext(requestMessages, chatHistoryProviderMessages!)
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, requestMessages, chatHistoryProviderMessages!)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
@@ -115,7 +115,7 @@ public sealed class ChatClientAgentSession : AgentSession
/// <summary>
/// Creates a new instance of the <see cref="ChatClientAgentSession"/> class from previously serialized state.
/// </summary>
/// <param name="serializedSessionState">A <see cref="JsonElement"/> representing the serialized state of the session.</param>
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the session.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="chatHistoryProviderFactory">
/// An optional factory function to create a custom <see cref="AI.ChatHistoryProvider"/> from its serialized state.
@@ -128,18 +128,18 @@ public sealed class ChatClientAgentSession : AgentSession
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the deserialized <see cref="ChatClientAgentSession"/>.</returns>
internal static async Task<ChatClientAgentSession> DeserializeAsync(
JsonElement serializedSessionState,
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatHistoryProvider>>? chatHistoryProviderFactory = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<AIContextProvider>>? aiContextProviderFactory = null,
CancellationToken cancellationToken = default)
{
if (serializedSessionState.ValueKind != JsonValueKind.Object)
if (serializedState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedSessionState));
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedState));
}
var state = serializedSessionState.Deserialize(
var state = serializedState.Deserialize(
AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(SessionState))) as SessionState;
var session = new ChatClientAgentSession();
@@ -165,7 +165,7 @@ public sealed class ChatClientAgentSession : AgentSession
}
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
internal JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
JsonElement? chatHistoryProviderState = this._chatHistoryProvider?.Serialize(jsonSerializerOptions);
@@ -15,7 +15,7 @@ public interface IAgentFixture : IAsyncLifetime
{
AIAgent Agent { get; }
Task<List<ChatMessage>> GetChatHistoryAsync(AgentSession session);
Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session);
Task DeleteSessionAsync(AgentSession session);
}
@@ -106,7 +106,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> creat
Assert.Contains("Paris", response1Text);
Assert.Contains("Vienna", response2Text);
var chatHistory = await this.Fixture.GetChatHistoryAsync(session);
var chatHistory = await this.Fixture.GetChatHistoryAsync(agent, session);
Assert.Equal(4, chatHistory.Count);
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.User));
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.Assistant));
@@ -111,7 +111,7 @@ public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFix
Assert.Contains("Paris", result1.Text);
Assert.Contains("Vienna", result2.Text);
var chatHistory = await this.Fixture.GetChatHistoryAsync(session);
var chatHistory = await this.Fixture.GetChatHistoryAsync(agent, session);
Assert.Equal(4, chatHistory.Count);
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.User));
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.Assistant));
@@ -35,7 +35,7 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture
public IChatClient ChatClient => this._agent.ChatClient;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentSession session)
public async Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session)
{
var typedSession = (ChatClientAgentSession)session;
@@ -44,7 +44,7 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture
return [];
}
return (await typedSession.ChatHistoryProvider.InvokingAsync(new([]))).ToList();
return (await typedSession.ChatHistoryProvider.InvokingAsync(new(agent, session, []))).ToList();
}
public Task<ChatClientAgent> CreateChatClientAgentAsync(
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Anthropic;
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;
using Anthropic.Models.Beta.Skills;
using Anthropic.Services;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
/// <summary>
/// Integration tests for Anthropic Skills functionality.
/// These tests are designed to be run locally with a valid Anthropic API key.
/// </summary>
public sealed class AnthropicSkillsIntegrationTests
{
// All tests for Anthropic are intended to be ran locally as the CI pipeline for Anthropic is not setup.
private const string SkipReason = "Integrations tests for local execution only";
private static readonly AnthropicConfiguration s_config = TestConfiguration.LoadSection<AnthropicConfiguration>();
[Fact(Skip = SkipReason)]
public async Task CreateAgentWithPptxSkillAsync()
{
// Arrange
AnthropicClient anthropicClient = new() { ApiKey = s_config.ApiKey };
string model = s_config.ChatModelId;
BetaSkillParams pptxSkill = new()
{
Type = BetaSkillParamsType.Anthropic,
SkillID = "pptx",
Version = "latest"
};
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
model: model,
instructions: "You are a helpful agent for creating PowerPoint presentations.",
tools: [pptxSkill.AsAITool()]);
// Act
AgentResponse response = await agent.RunAsync(
"Create a simple 2-slide presentation: a title slide and one content slide about AI.");
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Text);
Assert.NotEmpty(response.Text);
}
[Fact(Skip = SkipReason)]
public async Task ListAnthropicManagedSkillsAsync()
{
// Arrange
AnthropicClient anthropicClient = new() { ApiKey = s_config.ApiKey };
// Act
SkillListPage skills = await anthropicClient.Beta.Skills.List(
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
// Assert
Assert.NotNull(skills);
Assert.NotNull(skills.Items);
Assert.Contains(skills.Items, skill => skill.ID == "pptx");
}
}
@@ -33,7 +33,7 @@ public class AIProjectClientFixture : IChatClientAgentFixture
return response.Value.Id;
}
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentSession session)
public async Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session)
{
var chatClientSession = (ChatClientAgentSession)session;
@@ -53,7 +53,7 @@ public class AIProjectClientFixture : IChatClientAgentFixture
return [];
}
return (await chatClientSession.ChatHistoryProvider.InvokingAsync(new([]))).ToList();
return (await chatClientSession.ChatHistoryProvider.InvokingAsync(new(agent, session, []))).ToList();
}
private async Task<List<ChatMessage>> GetChatHistoryFromResponsesChainAsync(string conversationId)
@@ -24,7 +24,7 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture
public AIAgent Agent => this._agent;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentSession session)
public async Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session)
{
List<ChatMessage> messages = [];
var typedSession = (ChatClientAgentSession)session;
@@ -20,7 +20,7 @@ public class CopilotStudioFixture : IAgentFixture
{
public AIAgent Agent { get; private set; } = null!;
public Task<List<ChatMessage>> GetChatHistoryAsync(AgentSession session) =>
public Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session) =>
throw new NotSupportedException("CopilotStudio doesn't allow retrieval of chat history.");
public Task DeleteSessionAsync(AgentSession session) =>
@@ -251,7 +251,7 @@ public sealed class AGUIAgentTests
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: []);
AgentSession originalSession = await agent.CreateSessionAsync();
JsonElement serialized = originalSession.Serialize();
JsonElement serialized = agent.SerializeSession(originalSession);
// Act
AgentSession deserialized = await agent.DeserializeSessionAsync(serialized);
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AIAgentMetadata"/> class.
/// </summary>
public class AIAgentMetadataTests
{
[Fact]
public void Constructor_WithNoArguments_SetsProviderNameToNull()
{
// Arrange & Act
AIAgentMetadata metadata = new();
// Assert
Assert.Null(metadata.ProviderName);
}
[Fact]
public void Constructor_WithProviderName_SetsProperty()
{
// Arrange
const string ProviderName = "TestProvider";
// Act
AIAgentMetadata metadata = new(ProviderName);
// Assert
Assert.Equal(ProviderName, metadata.ProviderName);
}
[Fact]
public void Constructor_WithNullProviderName_SetsProviderNameToNull()
{
// Arrange & Act
AIAgentMetadata metadata = new(null);
// Assert
Assert.Null(metadata.ProviderName);
}
}
@@ -220,6 +220,133 @@ public class AIAgentTests
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Theory data for RunAsync overloads.
/// </summary>
public static TheoryData<string> RunAsyncOverloads => new()
{
"NoMessage",
"StringMessage",
"ChatMessage",
"MessagesCollection"
};
/// <summary>
/// Verifies that CurrentRunContext is properly set and accessible from RunCoreAsync for all RunAsync overloads.
/// </summary>
[Theory]
[MemberData(nameof(RunAsyncOverloads))]
public async Task RunAsync_SetsCurrentRunContext_AccessibleFromRunCoreAsync(string overload)
{
// Arrange
AgentRunContext? capturedContext = null;
var session = new TestAgentSession();
var options = new AgentRunOptions();
var agentMock = new Mock<AIAgent> { CallBase = true };
agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns((IEnumerable<ChatMessage> _, AgentSession? _, AgentRunOptions? _, CancellationToken _) =>
{
capturedContext = AIAgent.CurrentRunContext;
return Task.FromResult(new AgentResponse(new ChatMessage(ChatRole.Assistant, "Response")));
});
// Act
switch (overload)
{
case "NoMessage":
await agentMock.Object.RunAsync(session, options);
break;
case "StringMessage":
await agentMock.Object.RunAsync("Hello", session, options);
break;
case "ChatMessage":
await agentMock.Object.RunAsync(new ChatMessage(ChatRole.User, "Hello"), session, options);
break;
case "MessagesCollection":
await agentMock.Object.RunAsync([new ChatMessage(ChatRole.User, "Hello")], session, options);
break;
}
// Assert
Assert.NotNull(capturedContext);
Assert.Same(agentMock.Object, capturedContext!.Agent);
Assert.Same(session, capturedContext.Session);
Assert.Same(options, capturedContext.RunOptions);
if (overload == "NoMessage")
{
Assert.Empty(capturedContext.RequestMessages);
}
else
{
Assert.Single(capturedContext.RequestMessages);
}
}
/// <summary>
/// Verifies that CurrentRunContext is properly set and accessible from RunCoreStreamingAsync for all RunStreamingAsync overloads.
/// </summary>
[Theory]
[MemberData(nameof(RunAsyncOverloads))]
public async Task RunStreamingAsync_SetsCurrentRunContext_AccessibleFromRunCoreStreamingAsync(string overload)
{
// Arrange
AgentRunContext? capturedContext = null;
var session = new TestAgentSession();
var options = new AgentRunOptions();
var agentMock = new Mock<AIAgent> { CallBase = true };
agentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns((IEnumerable<ChatMessage> _, AgentSession? _, AgentRunOptions? _, CancellationToken _) =>
{
capturedContext = AIAgent.CurrentRunContext;
return ToAsyncEnumerableAsync([new AgentResponseUpdate(ChatRole.Assistant, "Response")]);
});
// Act
IAsyncEnumerable<AgentResponseUpdate> stream = overload switch
{
"NoMessage" => agentMock.Object.RunStreamingAsync(session, options),
"StringMessage" => agentMock.Object.RunStreamingAsync("Hello", session, options),
"ChatMessage" => agentMock.Object.RunStreamingAsync(new ChatMessage(ChatRole.User, "Hello"), session, options),
"MessagesCollection" => agentMock.Object.RunStreamingAsync(new[] { new ChatMessage(ChatRole.User, "Hello") }, session, options),
_ => throw new InvalidOperationException($"Unknown overload: {overload}")
};
await foreach (AgentResponseUpdate _ in stream)
{
// Consume the stream
}
// Assert
Assert.NotNull(capturedContext);
Assert.Same(agentMock.Object, capturedContext!.Agent);
Assert.Same(session, capturedContext.Session);
Assert.Same(options, capturedContext.RunOptions);
if (overload == "NoMessage")
{
Assert.Empty(capturedContext.RequestMessages);
}
else
{
Assert.Single(capturedContext.RequestMessages);
}
}
[Fact]
public void ValidateAgentIDIsIdempotent()
{
@@ -364,10 +491,78 @@ public class AIAgentTests
#endregion
#region Name and Description Property Tests
/// <summary>
/// Typed mock session.
/// Verify that Name property returns the value from the derived class.
/// </summary>
public abstract class TestAgentSession : AgentSession;
[Fact]
public void Name_ReturnsValueFromDerivedClass()
{
// Arrange
var agent = new MockAgentWithName("TestAgentName", "TestAgentDescription");
// Act
string? name = agent.Name;
// Assert
Assert.Equal("TestAgentName", name);
}
/// <summary>
/// Verify that Description property returns the value from the derived class.
/// </summary>
[Fact]
public void Description_ReturnsValueFromDerivedClass()
{
// Arrange
var agent = new MockAgentWithName("TestAgentName", "TestAgentDescription");
// Act
string? description = agent.Description;
// Assert
Assert.Equal("TestAgentDescription", description);
}
/// <summary>
/// Verify that Name property returns null when not overridden.
/// </summary>
[Fact]
public void Name_ReturnsNullByDefault()
{
// Arrange
var agent = new MockAgent();
// Act
string? name = agent.Name;
// Assert
Assert.Null(name);
}
/// <summary>
/// Verify that Description property returns null when not overridden.
/// </summary>
[Fact]
public void Description_ReturnsNullByDefault()
{
// Arrange
var agent = new MockAgent();
// Act
string? description = agent.Description;
// Assert
Assert.Null(description);
}
#endregion
/// <summary>
/// Typed mock session for testing purposes.
/// </summary>
private sealed class TestAgentSession : AgentSession;
private sealed class MockAgent : AIAgent
{
@@ -378,10 +573,51 @@ public class AIAgentTests
protected override string? IdCore { get; }
public override async ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override async ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
}
private sealed class MockAgentWithName : AIAgent
{
private readonly string? _name;
private readonly string? _description;
public MockAgentWithName(string? name, string? description)
{
this._name = name;
this._description = description;
}
public override string? Name => this._name;
public override string? Description => this._description;
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
@@ -1,21 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AIContextProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
[Fact]
public async Task InvokedAsync_ReturnsCompletedTaskAsync()
{
var provider = new TestAIContextProvider();
var messages = new ReadOnlyCollection<ChatMessage>([]);
var task = provider.InvokedAsync(new(messages, aiContextProviderMessages: null));
var task = provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null));
Assert.Equal(default, task);
}
@@ -30,13 +35,13 @@ public class AIContextProviderTests
[Fact]
public void InvokingContext_Constructor_ThrowsForNullMessages()
{
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(null!));
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullMessages()
{
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(null!, aiContextProviderMessages: null));
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, null!, aiContextProviderMessages: null));
}
#region GetService Method Tests
@@ -155,6 +160,209 @@ public class AIContextProviderTests
#endregion
#region InvokingContext Tests
[Fact]
public void InvokingContext_RequestMessages_SetterThrowsForNull()
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
}
[Fact]
public void InvokingContext_RequestMessages_SetterRoundtrips()
{
// Arrange
var initialMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, initialMessages);
// Act
context.RequestMessages = newMessages;
// Assert
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokingContext_Agent_ReturnsConstructorValue()
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokingContext_Session_ReturnsConstructorValue()
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokingContext_Session_CanBeNull()
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, null, messages);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(null!, s_mockSession, messages));
}
#endregion
#region InvokedContext Tests
[Fact]
public void InvokedContext_RequestMessages_SetterThrowsForNull()
{
// Arrange
var messages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, messages, aiContextProviderMessages: null);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
}
[Fact]
public void InvokedContext_RequestMessages_SetterRoundtrips()
{
// Arrange
var initialMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, initialMessages, aiContextProviderMessages: null);
// Act
context.RequestMessages = newMessages;
// Assert
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokedContext_AIContextProviderMessages_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var aiContextMessages = new List<ChatMessage> { new(ChatRole.System, "AI context message") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
// Act
context.AIContextProviderMessages = aiContextMessages;
// Assert
Assert.Same(aiContextMessages, context.AIContextProviderMessages);
}
[Fact]
public void InvokedContext_ResponseMessages_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response message") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
// Act
context.ResponseMessages = responseMessages;
// Assert
Assert.Same(responseMessages, context.ResponseMessages);
}
[Fact]
public void InvokedContext_InvokeException_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var exception = new InvalidOperationException("Test exception");
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
// Act
context.InvokeException = exception;
// Assert
Assert.Same(exception, context.InvokeException);
}
[Fact]
public void InvokedContext_Agent_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokedContext_Session_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokedContext_Session_CanBeNull()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, null, requestMessages, aiContextProviderMessages: null);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(null!, s_mockSession, requestMessages, aiContextProviderMessages: null));
}
#endregion
private sealed class TestAIContextProvider : AIContextProvider
{
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
@@ -230,4 +230,103 @@ public class AgentResponseTests
Assert.Equal(expectedResult.FullName, animal.FullName);
Assert.Equal(expectedResult.Species, animal.Species);
}
#endif
[Fact]
public void TryParseAsStructuredOutputWithJSOSuccess()
{
// Arrange.
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
// Act.
response.TryDeserialize(TestJsonSerializerContext.Default.Options, out Animal? animal);
// Assert.
Assert.NotNull(animal);
Assert.Equal(expectedResult.Id, animal.Id);
Assert.Equal(expectedResult.FullName, animal.FullName);
Assert.Equal(expectedResult.Species, animal.Species);
}
[Fact]
public void TryParseAsStructuredOutputFailsWithEmptyText()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, string.Empty));
// Act & Assert.
Assert.False(response.TryDeserialize<Animal>(TestJsonSerializerContext.Default.Options, out _));
}
[Fact]
public void TryParseAsStructuredOutputFailsWithIncorrectTypedJson()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "[]"));
// Act & Assert.
Assert.False(response.TryDeserialize<Animal>(TestJsonSerializerContext.Default.Options, out _));
}
[Fact]
public void ToAgentResponseUpdatesWithNoMessagesProducesEmptyArray()
{
// Arrange
AgentResponse response = new();
// Act
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
// Assert
Assert.Empty(updates);
}
[Fact]
public void ToAgentResponseUpdatesWithUsageOnlyProducesSingleUpdate()
{
// Arrange
AgentResponse response = new()
{
Usage = new UsageDetails { TotalTokenCount = 100 }
};
// Act
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
// Assert
AgentResponseUpdate update = Assert.Single(updates);
UsageContent usageContent = Assert.IsType<UsageContent>(update.Contents[0]);
Assert.Equal(100, usageContent.Details.TotalTokenCount);
}
[Fact]
public void ToAgentResponseUpdatesWithAdditionalPropertiesOnlyProducesSingleUpdate()
{
// Arrange
AgentResponse response = new()
{
AdditionalProperties = new() { ["key"] = "value" }
};
// Act
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
// Assert
AgentResponseUpdate update = Assert.Single(updates);
Assert.NotNull(update.AdditionalProperties);
Assert.Equal("value", update.AdditionalProperties!["key"]);
}
[Fact]
public void Deserialize_ThrowsWhenDeserializationReturnsNull()
{
// Arrange
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, "null"));
// Act & Assert
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(
() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
Assert.Equal("The deserialized response is null.", exception.Message);
}
}
@@ -299,6 +299,161 @@ public class AgentResponseUpdateExtensionsTests
Assert.Equal(expected, response.CreatedAt);
}
#region AsChatResponse Tests
[Fact]
public void AsChatResponse_WithNullArgument_ThrowsArgumentNullException()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>("response", () => ((AgentResponse)null!).AsChatResponse());
}
[Fact]
public void AsChatResponse_WithRawRepresentationAsChatResponse_ReturnsSameInstance()
{
// Arrange
ChatResponse originalChatResponse = new()
{
ResponseId = "original-response",
Messages = [new ChatMessage(ChatRole.Assistant, "Hello")]
};
AgentResponse agentResponse = new(originalChatResponse);
// Act
ChatResponse result = agentResponse.AsChatResponse();
// Assert
Assert.Same(originalChatResponse, result);
}
[Fact]
public void AsChatResponse_WithoutRawRepresentation_CreatesNewChatResponse()
{
// Arrange
AgentResponse agentResponse = new(new ChatMessage(ChatRole.Assistant, "Test message"))
{
ResponseId = "test-response-id",
CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
Usage = new UsageDetails { TotalTokenCount = 50 },
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
// Act
ChatResponse result = agentResponse.AsChatResponse();
// Assert
Assert.NotNull(result);
Assert.Equal("test-response-id", result.ResponseId);
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero), result.CreatedAt);
Assert.Same(agentResponse.Messages, result.Messages);
Assert.Same(agentResponse, result.RawRepresentation);
Assert.Same(agentResponse.Usage, result.Usage);
Assert.Same(agentResponse.AdditionalProperties, result.AdditionalProperties);
Assert.Equal(agentResponse.ContinuationToken, result.ContinuationToken);
}
#endregion
#region AsChatResponseUpdate Tests
[Fact]
public void AsChatResponseUpdate_WithNullArgument_ThrowsArgumentNullException()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>("responseUpdate", () => ((AgentResponseUpdate)null!).AsChatResponseUpdate());
}
[Fact]
public void AsChatResponseUpdate_WithRawRepresentationAsChatResponseUpdate_ReturnsSameInstance()
{
// Arrange
ChatResponseUpdate originalChatResponseUpdate = new()
{
ResponseId = "original-update",
Contents = [new TextContent("Hello")]
};
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
// Act
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
// Assert
Assert.Same(originalChatResponseUpdate, result);
}
[Fact]
public void AsChatResponseUpdate_WithoutRawRepresentation_CreatesNewChatResponseUpdate()
{
// Arrange
AgentResponseUpdate agentResponseUpdate = new(ChatRole.Assistant, "Test")
{
AuthorName = "TestAuthor",
ResponseId = "update-id",
MessageId = "message-id",
CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
// Act
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
// Assert
Assert.NotNull(result);
Assert.Equal("TestAuthor", result.AuthorName);
Assert.Equal("update-id", result.ResponseId);
Assert.Equal("message-id", result.MessageId);
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero), result.CreatedAt);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Same(agentResponseUpdate.Contents, result.Contents);
Assert.Same(agentResponseUpdate, result.RawRepresentation);
Assert.Same(agentResponseUpdate.AdditionalProperties, result.AdditionalProperties);
Assert.Equal(agentResponseUpdate.ContinuationToken, result.ContinuationToken);
}
#endregion
#region AsChatResponseUpdatesAsync Tests
[Fact]
public async Task AsChatResponseUpdatesAsync_WithNullArgument_ThrowsArgumentNullExceptionAsync()
{
// Arrange & Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>("responseUpdates", async () =>
{
await foreach (ChatResponseUpdate _ in ((IAsyncEnumerable<AgentResponseUpdate>)null!).AsChatResponseUpdatesAsync())
{
// Do nothing
}
});
}
[Fact]
public async Task AsChatResponseUpdatesAsync_ConvertsUpdatesAsync()
{
// Arrange
AgentResponseUpdate[] updates =
[
new(ChatRole.Assistant, "First"),
new(ChatRole.Assistant, "Second"),
];
// Act
List<ChatResponseUpdate> results = [];
await foreach (ChatResponseUpdate update in YieldAsync(updates).AsChatResponseUpdatesAsync())
{
results.Add(update);
}
// Assert
Assert.Equal(2, results.Count);
Assert.Equal("First", Assert.IsType<TextContent>(results[0].Contents[0]).Text);
Assert.Equal("Second", Assert.IsType<TextContent>(results[1].Contents[0]).Text);
}
#endregion
private static async IAsyncEnumerable<AgentResponseUpdate> YieldAsync(IEnumerable<AgentResponseUpdate> updates)
{
foreach (AgentResponseUpdate update in updates)
@@ -0,0 +1,233 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AgentRunContext"/> class.
/// </summary>
public sealed class AgentRunContextTests
{
#region Constructor Validation Tests
/// <summary>
/// Verifies that passing null for agent throws ArgumentNullException.
/// </summary>
[Fact]
public void Constructor_NullAgent_ThrowsArgumentNullException()
{
// Arrange
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRunContext(null!, session, messages, options));
}
/// <summary>
/// Verifies that passing null for session does not throw
/// </summary>
[Fact]
public void Constructor_NullSession_DoesNotThrow()
{
// Arrange
AIAgent agent = new TestAgent();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, null, messages, options);
// Assert
Assert.NotNull(context);
Assert.Null(context.Session);
}
/// <summary>
/// Verifies that passing null for requestMessages throws ArgumentNullException.
/// </summary>
[Fact]
public void Constructor_NullRequestMessages_ThrowsArgumentNullException()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
AgentRunOptions options = new();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRunContext(agent, session, null!, options));
}
/// <summary>
/// Verifies that passing null for agentRunOptions does not throw.
/// </summary>
[Fact]
public void Constructor_NullAgentRunOptions_DoesNotThrow()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
// Act
AgentRunContext context = new(agent, session, messages, null);
// Assert
Assert.NotNull(context);
Assert.Null(context.RunOptions);
}
#endregion
#region Property Roundtrip Tests
/// <summary>
/// Verifies that the Agent property returns the value passed to the constructor.
/// </summary>
[Fact]
public void Agent_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(agent, context.Agent);
}
/// <summary>
/// Verifies that the Session property returns the value passed to the constructor.
/// </summary>
[Fact]
public void Session_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(session, context.Session);
}
/// <summary>
/// Verifies that the RequestMessages property returns the value passed to the constructor.
/// </summary>
[Fact]
public void RequestMessages_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(messages, context.RequestMessages);
Assert.Equal(2, context.RequestMessages.Count);
}
/// <summary>
/// Verifies that the RunOptions property returns the value passed to the constructor.
/// </summary>
[Fact]
public void RunOptions_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new()
{
AllowBackgroundResponses = true,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["key1"] = "value1"
}
};
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(options, context.RunOptions);
Assert.True(context.RunOptions!.AllowBackgroundResponses);
}
/// <summary>
/// Verifies that an empty messages collection is handled correctly.
/// </summary>
[Fact]
public void RequestMessages_EmptyCollection_ReturnsEmptyCollection()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.NotNull(context.RequestMessages);
Assert.Empty(context.RequestMessages);
}
#endregion
#region Test Helpers
private sealed class TestAgentSession : AgentSession;
private sealed class TestAgent : AIAgent
{
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
}
#endregion
}
@@ -11,14 +11,6 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// </summary>
public class AgentSessionTests
{
[Fact]
public void Serialize_ReturnsDefaultJsonElement()
{
var session = new TestAgentSession();
var result = session.Serialize();
Assert.Equal(default, result);
}
#region GetService Method Tests
/// <summary>
@@ -14,6 +14,9 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// </summary>
public sealed class ChatHistoryProviderExtensionsTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
[Fact]
public void WithMessageFilters_ReturnsChatHistoryProviderMessageFilter()
{
@@ -35,7 +38,7 @@ public sealed class ChatHistoryProviderExtensionsTests
// Arrange
Mock<ChatHistoryProvider> providerMock = new();
List<ChatMessage> innerMessages = [new(ChatRole.User, "Hello"), new(ChatRole.Assistant, "Hi")];
ChatHistoryProvider.InvokingContext context = new([new ChatMessage(ChatRole.User, "Test")]);
ChatHistoryProvider.InvokingContext context = new(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
providerMock
.Setup(p => p.InvokingAsync(context, It.IsAny<CancellationToken>()))
@@ -59,7 +62,7 @@ public sealed class ChatHistoryProviderExtensionsTests
Mock<ChatHistoryProvider> providerMock = new();
List<ChatMessage> requestMessages = [new(ChatRole.User, "Hello")];
List<ChatMessage> chatHistoryProviderMessages = [new(ChatRole.System, "System")];
ChatHistoryProvider.InvokedContext context = new(requestMessages, chatHistoryProviderMessages)
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages, chatHistoryProviderMessages)
{
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
};
@@ -106,7 +109,7 @@ public sealed class ChatHistoryProviderExtensionsTests
List<ChatMessage> requestMessages = [new(ChatRole.User, "Hello")];
List<ChatMessage> chatHistoryProviderMessages = [new(ChatRole.System, "System")];
List<ChatMessage> aiContextProviderMessages = [new(ChatRole.System, "Context")];
ChatHistoryProvider.InvokedContext context = new(requestMessages, chatHistoryProviderMessages)
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages, chatHistoryProviderMessages)
{
AIContextProviderMessages = aiContextProviderMessages
};
@@ -16,6 +16,9 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// </summary>
public sealed class ChatHistoryProviderMessageFilterTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
[Fact]
public void Constructor_WithNullInnerProvider_ThrowsArgumentNullException()
{
@@ -59,7 +62,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
var context = new ChatHistoryProvider.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
innerProviderMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
@@ -88,7 +91,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
new(ChatRole.Assistant, "Hi there!"),
new(ChatRole.User, "How are you?")
};
var context = new ChatHistoryProvider.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
innerProviderMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
@@ -118,7 +121,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
var context = new ChatHistoryProvider.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
innerProviderMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
@@ -147,7 +150,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var chatHistoryProviderMessages = new List<ChatMessage> { new(ChatRole.System, "System") };
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response") };
var context = new ChatHistoryProvider.InvokedContext(requestMessages, chatHistoryProviderMessages)
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, chatHistoryProviderMessages)
{
ResponseMessages = responseMessages
};
@@ -162,7 +165,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
ChatHistoryProvider.InvokedContext InvokedFilter(ChatHistoryProvider.InvokedContext ctx)
{
var modifiedRequestMessages = ctx.RequestMessages.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
return new ChatHistoryProvider.InvokedContext(modifiedRequestMessages, ctx.ChatHistoryProviderMessages)
return new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, modifiedRequestMessages, ctx.ChatHistoryProviderMessages)
{
ResponseMessages = ctx.ResponseMessages,
AIContextProviderMessages = ctx.AIContextProviderMessages,
@@ -6,6 +6,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -14,6 +15,9 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// </summary>
public class ChatHistoryProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
#region GetService Method Tests
[Fact]
@@ -76,6 +80,238 @@ public class ChatHistoryProviderTests
#endregion
#region InvokingContext Tests
[Fact]
public void InvokingContext_Constructor_ThrowsForNullMessages()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokingContext_RequestMessages_SetterThrowsForNull()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
}
[Fact]
public void InvokingContext_RequestMessages_SetterRoundtrips()
{
// Arrange
var initialMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, initialMessages);
// Act
context.RequestMessages = newMessages;
// Assert
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokingContext_Agent_ReturnsConstructorValue()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokingContext_Session_ReturnsConstructorValue()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokingContext_Session_CanBeNull()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, null, messages);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokingContext(null!, s_mockSession, messages));
}
#endregion
#region InvokedContext Tests
[Fact]
public void InvokedContext_Constructor_ThrowsForNullRequestMessages()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, null!, []));
}
[Fact]
public void InvokedContext_RequestMessages_SetterThrowsForNull()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
}
[Fact]
public void InvokedContext_RequestMessages_SetterRoundtrips()
{
// Arrange
var initialMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, initialMessages, []);
// Act
context.RequestMessages = newMessages;
// Assert
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokedContext_ChatHistoryProviderMessages_SetterRoundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var newProviderMessages = new List<ChatMessage> { new(ChatRole.System, "System message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act
context.ChatHistoryProviderMessages = newProviderMessages;
// Assert
Assert.Same(newProviderMessages, context.ChatHistoryProviderMessages);
}
[Fact]
public void InvokedContext_AIContextProviderMessages_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var aiContextMessages = new List<ChatMessage> { new(ChatRole.System, "AI context message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act
context.AIContextProviderMessages = aiContextMessages;
// Assert
Assert.Same(aiContextMessages, context.AIContextProviderMessages);
}
[Fact]
public void InvokedContext_ResponseMessages_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response message") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act
context.ResponseMessages = responseMessages;
// Assert
Assert.Same(responseMessages, context.ResponseMessages);
}
[Fact]
public void InvokedContext_InvokeException_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var exception = new InvalidOperationException("Test exception");
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Act
context.InvokeException = exception;
// Assert
Assert.Same(exception, context.InvokeException);
}
[Fact]
public void InvokedContext_Agent_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokedContext_Session_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokedContext_Session_CanBeNull()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, null, requestMessages, []);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(null!, s_mockSession, requestMessages, []));
}
#endregion
private sealed class TestChatHistoryProvider : ChatHistoryProvider
{
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -26,7 +27,7 @@ public class DelegatingAIAgentTests
/// </summary>
public DelegatingAIAgentTests()
{
this._innerAgentMock = new Mock<AIAgent>();
this._innerAgentMock = new Mock<AIAgent> { CallBase = true };
this._testResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
this._testStreamingResponses = [new AgentResponseUpdate(ChatRole.Assistant, "Test streaming response")];
this._testSession = new TestAgentSession();
@@ -35,7 +36,10 @@ public class DelegatingAIAgentTests
this._innerAgentMock.Protected().SetupGet<string>("IdCore").Returns("test-agent-id");
this._innerAgentMock.Setup(x => x.Name).Returns("Test Agent");
this._innerAgentMock.Setup(x => x.Description).Returns("Test Description");
this._innerAgentMock.Setup(x => x.CreateSessionAsync()).ReturnsAsync(this._testSession);
this._innerAgentMock
.Protected()
.Setup<ValueTask<AgentSession>>("CreateSessionCoreAsync", ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._testSession);
this._innerAgentMock
.Protected()
@@ -142,7 +146,32 @@ public class DelegatingAIAgentTests
// Assert
Assert.Same(this._testSession, session);
this._innerAgentMock.Verify(x => x.CreateSessionAsync(), Times.Once);
this._innerAgentMock
.Protected()
.Verify<ValueTask<AgentSession>>("CreateSessionCoreAsync", Times.Once(), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
/// Verify that DeserializeSessionAsync delegates to inner agent.
/// </summary>
[Fact]
public async Task DeserializeSessionAsync_DelegatesToInnerAgentAsync()
{
// Arrange
var serializedSession = JsonSerializer.SerializeToElement("test-session-id", TestJsonSerializerContext.Default.String);
this._innerAgentMock
.Protected()
.Setup<ValueTask<AgentSession>>("DeserializeSessionCoreAsync", ItExpr.IsAny<JsonElement>(), ItExpr.IsAny<JsonSerializerOptions?>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._testSession);
// Act
var session = await this._delegatingAgent.DeserializeSessionAsync(serializedSession);
// Assert
Assert.Same(this._testSession, session);
this._innerAgentMock
.Protected()
.Verify<ValueTask<AgentSession>>("DeserializeSessionCoreAsync", Times.Once(), ItExpr.IsAny<JsonElement>(), ItExpr.IsAny<JsonSerializerOptions?>(), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
@@ -18,6 +18,9 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// </summary>
public class InMemoryChatHistoryProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
[Fact]
public void Constructor_Throws_ForNullReducer() =>
// Arrange & Act & Assert
@@ -68,7 +71,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider();
provider.Add(providerMessages[0]);
var context = new ChatHistoryProvider.InvokedContext(requestMessages, providerMessages)
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, providerMessages)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
@@ -87,7 +90,7 @@ public class InMemoryChatHistoryProviderTests
{
var provider = new InMemoryChatHistoryProvider();
var context = new ChatHistoryProvider.InvokedContext([], []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [], []);
await provider.InvokedAsync(context, CancellationToken.None);
Assert.Empty(provider);
@@ -102,7 +105,7 @@ public class InMemoryChatHistoryProviderTests
new ChatMessage(ChatRole.Assistant, "Test2")
};
var context = new ChatHistoryProvider.InvokingContext([]);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var result = (await provider.InvokingAsync(context, CancellationToken.None)).ToList();
Assert.Equal(2, result.Count);
@@ -183,7 +186,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider();
var messages = new List<ChatMessage>();
var context = new ChatHistoryProvider.InvokedContext(messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
await provider.InvokedAsync(context, CancellationToken.None);
Assert.Empty(provider);
@@ -520,7 +523,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded);
// Act
var context = new ChatHistoryProvider.InvokedContext(originalMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, originalMessages, []);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
@@ -556,7 +559,7 @@ public class InMemoryChatHistoryProviderTests
}
// Act
var invokingContext = new ChatHistoryProvider.InvokingContext(Array.Empty<ChatMessage>());
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, Array.Empty<ChatMessage>());
var result = (await provider.InvokingAsync(invokingContext, CancellationToken.None)).ToList();
// Assert
@@ -579,7 +582,7 @@ public class InMemoryChatHistoryProviderTests
var provider = new InMemoryChatHistoryProvider(reducerMock.Object, InMemoryChatHistoryProvider.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
// Act
var context = new ChatHistoryProvider.InvokedContext(originalMessages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, originalMessages, []);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
@@ -605,7 +608,7 @@ public class InMemoryChatHistoryProviderTests
};
// Act
var invokingContext = new ChatHistoryProvider.InvokingContext(Array.Empty<ChatMessage>());
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, Array.Empty<ChatMessage>());
var result = (await provider.InvokingAsync(invokingContext, CancellationToken.None)).ToList();
// Assert
@@ -614,6 +617,42 @@ public class InMemoryChatHistoryProviderTests
reducerMock.Verify(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task InvokedAsync_WithException_DoesNotAddMessagesAsync()
{
// Arrange
var provider = new InMemoryChatHistoryProvider();
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var responseMessages = new List<ChatMessage>
{
new(ChatRole.Assistant, "Hi there!")
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, [])
{
ResponseMessages = responseMessages,
InvokeException = new InvalidOperationException("Test exception")
};
// Act
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.Empty(provider);
}
[Fact]
public async Task InvokingAsync_WithNullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new InMemoryChatHistoryProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokingAsync(null!, CancellationToken.None).AsTask());
}
public class TestAIContent(string testData) : AIContent
{
public string TestData => testData;
@@ -2384,6 +2384,134 @@ public sealed class AzureAIProjectChatClientExtensionsTests
#endregion
#region Empty Version and ID Handling Tests
/// <summary>
/// Verify that GetAIAgentAsync handles an agent with empty version by using "latest" as fallback.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithEmptyVersion_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
// Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion();
// Act
var agent = client.AsAIAgent(agentRecord);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent record name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent version name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that GetAIAgentAsync handles an agent with whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithWhitespaceVersion_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
// Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion();
// Act
var agent = client.AsAIAgent(agentRecord);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent record name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent version name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
#endregion
#region ApplyToolsToAgentDefinition Tests
/// <summary>
@@ -2678,6 +2806,54 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents.
/// </summary>
private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true);
}
/// <summary>
/// Creates a test AgentRecord with empty version for testing hosted MCP agents.
/// </summary>
private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AgentVersion with empty version for testing hosted MCP agents.
/// </summary>
private AgentVersion CreateTestAgentVersionWithEmptyVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!;
}
/// <summary>
/// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents.
/// </summary>
private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace);
}
/// <summary>
/// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents.
/// </summary>
private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents.
/// </summary>
private AgentVersion CreateTestAgentVersionWithWhitespaceVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!;
}
private const string OpenAPISpec = """
{
"openapi": "3.0.3",
@@ -2716,14 +2892,26 @@ public sealed class AzureAIProjectChatClientExtensionsTests
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
}
/// <summary>
/// Specifies the version mode for test data generation.
/// </summary>
private enum VersionMode
{
Normal,
Empty,
Whitespace
}
/// <summary>
/// Fake AIProjectClient for testing.
/// </summary>
private sealed class FakeAgentClient : AIProjectClient
{
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal)
{
this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse);
// Handle backward compatibility with bool parameter
var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode;
this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode);
}
public override ClientConnection GetConnection(string connectionId)
@@ -2739,60 +2927,82 @@ public sealed class AzureAIProjectChatClientExtensionsTests
private readonly string? _instructions;
private readonly string? _description;
private readonly AgentDefinition? _agentDefinition;
private readonly VersionMode _versionMode;
public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal)
{
this._agentName = agentName;
this._instructions = instructions;
this._description = description;
this._agentDefinition = agentDefinitionResponse;
this._versionMode = versionMode;
}
private string GetAgentResponseJson()
{
return this._versionMode switch
{
VersionMode.Empty => TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
VersionMode.Whitespace => TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
_ => TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description)
};
}
private string GetAgentVersionResponseJson()
{
return this._versionMode switch
{
VersionMode.Empty => TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
VersionMode.Whitespace => TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
_ => TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description)
};
}
public override ClientResult GetAgent(string agentName, RequestOptions options)
{
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentRecord> GetAgent(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> GetAgentAsync(string agentName, RequestOptions options)
{
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentResponseJson();
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentRecord>> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
}
@@ -52,6 +52,70 @@ internal static class TestDataUtil
return json;
}
/// <summary>
/// Gets the agent version response JSON with empty version and ID fields for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentVersionResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
// Remove the version and id fields to simulate hosted agents without version
json = json.Replace("\"version\": \"1\",", "\"version\": \"\",");
json = json.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \"\",");
return json;
}
/// <summary>
/// Gets the agent response JSON with empty version and ID fields in the latest version for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
// Remove the version and id fields to simulate hosted agents without version
json = json.Replace("\"version\": \"1\",", "\"version\": \"\",");
json = json.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \"\",");
return json;
}
/// <summary>
/// Gets the agent version response JSON with whitespace-only version and ID fields for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentVersionResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentVersionResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
// Use whitespace-only version and id fields to simulate hosted agents without version
return json
.Replace("\"version\": \"1\",", "\"version\": \" \",")
.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \" \",");
}
/// <summary>
/// Gets the agent response JSON with whitespace-only version and ID fields in the latest version for testing hosted agents like MCP agents.
/// </summary>
public static string GetAgentResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
// Use whitespace-only version and id fields to simulate hosted agents without version
return json
.Replace("\"version\": \"1\",", "\"version\": \" \",")
.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \" \",");
}
/// <summary>
/// Gets the OpenAI default response JSON with optional placeholder replacements applied.
/// </summary>
@@ -41,6 +41,9 @@ namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
[Collection("CosmosDB")]
public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
{
private static readonly AIAgent s_mockAgent = new Moq.Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Moq.Mock<AgentSession>().Object;
// Cosmos DB Emulator connection settings
private const string EmulatorEndpoint = "https://localhost:8081";
private const string EmulatorKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
@@ -214,7 +217,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
var message = new ChatMessage(ChatRole.User, "Hello, world!");
var context = new ChatHistoryProvider.InvokedContext([message], [])
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [message], [])
{
ResponseMessages = []
};
@@ -226,7 +229,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(100);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var messages = await provider.InvokingAsync(invokingContext);
var messageList = messages.ToList();
@@ -293,7 +296,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
new ChatMessage(ChatRole.Assistant, "Response message")
};
var context = new ChatHistoryProvider.InvokedContext(requestMessages, [])
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, [])
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
@@ -303,7 +306,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await provider.InvokedAsync(context);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
Assert.Equal(5, messageList.Count);
@@ -327,7 +330,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString());
// Act
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var messages = await provider.InvokingAsync(invokingContext);
// Assert
@@ -346,15 +349,15 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var store1 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation1);
using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation2);
var context1 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 2")], []);
var context1 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message for conversation 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message for conversation 2")], []);
await store1.InvokedAsync(context1);
await store2.InvokedAsync(context2);
// Act
var invokingContext1 = new ChatHistoryProvider.InvokingContext([]);
var invokingContext2 = new ChatHistoryProvider.InvokingContext([]);
var invokingContext1 = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var invokingContext2 = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var messages1 = await store1.InvokingAsync(invokingContext1);
var messages2 = await store2.InvokingAsync(invokingContext2);
@@ -391,11 +394,11 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
};
// Act 1: Add messages
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, []);
var invokedContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
await originalStore.InvokedAsync(invokedContext);
// Act 2: Verify messages were added
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var retrievedMessages = await originalStore.InvokingAsync(invokingContext);
var retrievedList = retrievedMessages.ToList();
Assert.Equal(5, retrievedList.Count);
@@ -545,7 +548,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var message = new ChatMessage(ChatRole.User, "Hello from hierarchical partitioning!");
var context = new ChatHistoryProvider.InvokedContext([message], []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [message], []);
// Act
await provider.InvokedAsync(context);
@@ -554,7 +557,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(100);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var messages = await provider.InvokingAsync(invokingContext);
var messageList = messages.ToList();
@@ -602,7 +605,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
new ChatMessage(ChatRole.User, "Third hierarchical message")
};
var context = new ChatHistoryProvider.InvokedContext(messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
// Act
await provider.InvokedAsync(context);
@@ -611,7 +614,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(100);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
@@ -637,8 +640,8 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId2, SessionId);
// Add messages to both stores
var context1 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 2")], []);
var context1 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message from user 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Message from user 2")], []);
await store1.InvokedAsync(context1);
await store2.InvokedAsync(context2);
@@ -647,8 +650,8 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(100);
// Act & Assert
var invokingContext1 = new ChatHistoryProvider.InvokingContext([]);
var invokingContext2 = new ChatHistoryProvider.InvokingContext([]);
var invokingContext1 = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var invokingContext2 = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var messages1 = await store1.InvokingAsync(invokingContext1);
var messageList1 = messages1.ToList();
@@ -675,7 +678,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var originalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var context = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Test serialization message")], []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test serialization message")], []);
await originalStore.InvokedAsync(context);
// Act - Serialize the provider state
@@ -693,7 +696,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(100);
// Assert - The deserialized provider should have the same functionality
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var messages = await deserializedStore.InvokingAsync(invokingContext);
var messageList = messages.ToList();
@@ -717,8 +720,8 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
using var hierarchicalProvider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-coexist", "user-coexist", SessionId);
// Add messages to both
var simpleContext = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Simple partitioning message")], []);
var hierarchicalContext = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []);
var simpleContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Simple partitioning message")], []);
var hierarchicalContext = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []);
await simpleProvider.InvokedAsync(simpleContext);
await hierarchicalProvider.InvokedAsync(hierarchicalContext);
@@ -727,7 +730,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(100);
// Act & Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var simpleMessages = await simpleProvider.InvokingAsync(invokingContext);
var simpleMessageList = simpleMessages.ToList();
@@ -760,7 +763,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
await Task.Delay(10); // Small delay to ensure different timestamps
}
var context = new ChatHistoryProvider.InvokedContext(messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
await provider.InvokedAsync(context);
// Wait for eventual consistency
@@ -768,7 +771,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
// Act - Set max to 5 and retrieve
provider.MaxMessagesToRetrieve = 5;
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
@@ -798,14 +801,14 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
messages.Add(new ChatMessage(ChatRole.User, $"Message {i}"));
}
var context = new ChatHistoryProvider.InvokedContext(messages, []);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, []);
await provider.InvokedAsync(context);
// Wait for eventual consistency
await Task.Delay(100);
// Act - No limit set (default null)
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
@@ -66,12 +66,17 @@ public sealed class AggregatorPromptAgentFactoryTests
private sealed class TestAgent : AIAgent
{
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
throw new NotImplementedException();
}
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
@@ -10,7 +10,7 @@ public sealed class DurableAgentSessionTests
public void BuiltInSerialization()
{
AgentSessionId sessionId = AgentSessionId.WithRandomKey("test-agent");
AgentSession session = new DurableAgentSession(sessionId);
DurableAgentSession session = new(sessionId);
JsonElement serializedSession = session.Serialize();
@@ -175,7 +175,10 @@ public sealed class AIAgentExtensionsTests
{
Mock<AIAgent> agentMock = new() { CallBase = true };
agentMock.SetupGet(x => x.Name).Returns("TestAgent");
agentMock.Setup(x => x.CreateSessionAsync()).ReturnsAsync(new TestAgentSession());
agentMock
.Protected()
.Setup<ValueTask<AgentSession>>("CreateSessionCoreAsync", ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new TestAgentSession());
agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
@@ -194,7 +197,10 @@ public sealed class AIAgentExtensionsTests
{
Mock<AIAgent> agentMock = new() { CallBase = true };
agentMock.SetupGet(x => x.Name).Returns("TestAgent");
agentMock.Setup(x => x.CreateSessionAsync()).ReturnsAsync(new TestAgentSession());
agentMock
.Protected()
.Setup<ValueTask<AgentSession>>("CreateSessionCoreAsync", ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new TestAgentSession());
agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
@@ -280,11 +280,14 @@ internal sealed class FakeChatClientAgent : AIAgent
public override string? Description => "A fake agent for testing";
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) =>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedSession, jsonSerializerOptions));
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedState, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
protected override async Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
@@ -344,11 +347,21 @@ internal sealed class FakeMultiMessageAgent : AIAgent
public override string? Description => "A fake agent that sends multiple messages for testing";
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) =>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedSession, jsonSerializerOptions));
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedState, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is not FakeInMemoryAgentSession fakeSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return fakeSession.Serialize(jsonSerializerOptions);
}
protected override async Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
@@ -425,6 +438,8 @@ internal sealed class FakeMultiMessageAgent : AIAgent
: base(serializedSession, jsonSerializerOptions)
{
}
internal new JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> base.Serialize(jsonSerializerOptions);
}
public override object? GetService(Type serviceType, object? serviceKey = null) => null;
@@ -334,11 +334,21 @@ internal sealed class FakeForwardedPropsAgent : AIAgent
await Task.CompletedTask;
}
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) =>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedSession, jsonSerializerOptions));
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedState, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is not FakeInMemoryAgentSession fakeSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return fakeSession.Serialize(jsonSerializerOptions);
}
private sealed class FakeInMemoryAgentSession : InMemoryAgentSession
{
@@ -351,6 +361,9 @@ internal sealed class FakeForwardedPropsAgent : AIAgent
: base(serializedSession, jsonSerializerOptions)
{
}
internal new JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> base.Serialize(jsonSerializerOptions);
}
public override object? GetService(Type serviceType, object? serviceKey = null) => null;
@@ -417,11 +417,21 @@ internal sealed class FakeStateAgent : AIAgent
await Task.CompletedTask;
}
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) =>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedSession, jsonSerializerOptions));
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentSession(serializedState, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is not FakeInMemoryAgentSession fakeSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return fakeSession.Serialize(jsonSerializerOptions);
}
private sealed class FakeInMemoryAgentSession : InMemoryAgentSession
{
@@ -434,6 +444,9 @@ internal sealed class FakeStateAgent : AIAgent
: base(serializedSession, jsonSerializerOptions)
{
}
internal new JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> base.Serialize(jsonSerializerOptions);
}
public override object? GetService(Type serviceType, object? serviceKey = null) => null;
@@ -425,11 +425,21 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
public override string? Description => "Agent that produces multiple text chunks";
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) =>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentSession(serializedSession, jsonSerializerOptions));
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentSession(serializedState, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is not TestInMemoryAgentSession testSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return testSession.Serialize(jsonSerializerOptions);
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
@@ -507,6 +517,9 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
: base(serializedSessionState, jsonSerializerOptions, null)
{
}
internal new JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> base.Serialize(jsonSerializerOptions);
}
private sealed class TestAgent : AIAgent
@@ -515,11 +528,21 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
public override string? Description => "Test agent";
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) =>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentSession(serializedSession, jsonSerializerOptions));
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentSession(serializedState, jsonSerializerOptions));
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (session is not TestInMemoryAgentSession testSession)
{
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
}
return testSession.Serialize(jsonSerializerOptions);
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
@@ -11,10 +11,13 @@ internal sealed class TestAgent(string name, string description) : AIAgent
public override string? Description => description;
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default) => new(new DummyAgentSession());
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) => new(new DummyAgentSession());
public override ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedSession,
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => new(new DummyAgentSession());
protected override Task<AgentResponse> RunCoreAsync(
@@ -18,6 +18,9 @@ public sealed class Mem0ProviderTests : IDisposable
{
private const string SkipReason = "Requires a Mem0 service configured"; // Set to null to enable.
private static readonly AIAgent s_mockAgent = new Moq.Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Moq.Mock<AgentSession>().Object;
private readonly HttpClient _httpClient;
public Mem0ProviderTests()
@@ -49,14 +52,14 @@ public sealed class Mem0ProviderTests : IDisposable
var sut = new Mem0Provider(this._httpClient, storageScope);
await sut.ClearStoredMemoriesAsync();
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext([input], aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [input], aiContextProviderMessages: null));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
await sut.ClearStoredMemoriesAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
@@ -73,14 +76,14 @@ public sealed class Mem0ProviderTests : IDisposable
var sut = new Mem0Provider(this._httpClient, storageScope);
await sut.ClearStoredMemoriesAsync();
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [assistantIntro], aiContextProviderMessages: null));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
await sut.ClearStoredMemoriesAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
// Assert
Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
@@ -99,13 +102,13 @@ public sealed class Mem0ProviderTests : IDisposable
await sut1.ClearStoredMemoriesAsync();
await sut2.ClearStoredMemoriesAsync();
var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]));
Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?[0].Text ?? string.Empty);
Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
// Act
await sut1.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
await sut1.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [assistantIntro], aiContextProviderMessages: null));
var ctxAfterAdding1 = await GetContextWithRetryAsync(sut1, question);
var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, question);
@@ -123,7 +126,7 @@ public sealed class Mem0ProviderTests : IDisposable
AIContext? ctx = null;
for (int i = 0; i < attempts; i++)
{
ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext([question]), CancellationToken.None);
ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [question]), CancellationToken.None);
var text = ctx.Messages?[0].Text;
if (!string.IsNullOrEmpty(text) && text.IndexOf("Caoimhe", StringComparison.OrdinalIgnoreCase) >= 0)
{
@@ -18,6 +18,9 @@ namespace Microsoft.Agents.AI.Mem0.UnitTests;
/// </summary>
public sealed class Mem0ProviderTests : IDisposable
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
private readonly Mock<ILogger<Mem0Provider>> _loggerMock;
private readonly Mock<ILoggerFactory> _loggerFactoryMock;
private readonly RecordingHandler _handler = new();
@@ -96,7 +99,7 @@ public sealed class Mem0ProviderTests : IDisposable
UserId = "user"
};
var sut = new Mem0Provider(this._httpClient, storageScope, options: new() { EnableSensitiveTelemetryData = true }, loggerFactory: this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "What is my name?")]);
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "What is my name?")]);
// Act
var aiContext = await sut.InvokingAsync(invokingContext);
@@ -161,7 +164,7 @@ public sealed class Mem0ProviderTests : IDisposable
var options = new Mem0ProviderOptions { EnableSensitiveTelemetryData = enableSensitiveTelemetryData };
var sut = new Mem0Provider(this._httpClient, storageScope, options: options, loggerFactory: this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Who am I?")]);
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Who am I?")]);
// Act
await sut.InvokingAsync(invokingContext, CancellationToken.None);
@@ -215,7 +218,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
// Assert
var memoryPosts = this._handler.Requests.Where(r => r.RequestMessage.RequestUri!.AbsolutePath == "/v1/memories/" && r.RequestMessage.Method == HttpMethod.Post).ToList();
@@ -242,7 +245,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = null, InvokeException = new InvalidOperationException("Request Failed") });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = null, InvokeException = new InvalidOperationException("Request Failed") });
// Assert
Assert.Empty(this._handler.Requests);
@@ -268,7 +271,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
// Assert
this._loggerMock.Verify(
@@ -318,7 +321,7 @@ public sealed class Mem0ProviderTests : IDisposable
};
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
// Assert
Assert.Equal(expectedLogCount, this._loggerMock.Invocations.Count);
@@ -400,7 +403,7 @@ public sealed class Mem0ProviderTests : IDisposable
// Arrange
var storageScope = new Mem0ProviderScope { ApplicationId = "app" };
var provider = new Mem0Provider(this._httpClient, storageScope, loggerFactory: this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext([new ChatMessage(ChatRole.User, "Q?")]);
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Q?")]);
// Act
var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None);
@@ -418,6 +418,51 @@ public class AIAgentBuilderTests
Assert.IsType<AnonymousDelegatingAIAgent>(result);
}
/// <summary>
/// Verify that Use with both delegates allows both to access AgentRunContext.
/// </summary>
[Fact]
public async Task Use_WithBothDelegates_AllowsDelegateToAccessAgentRunContextAsync()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var mockSession = new Mock<AgentSession>();
var builder = new AIAgentBuilder(mockAgent.Object);
AIAgent? builtAgent = null;
bool nonStreamingMiddlewareExecuted = false;
bool streamingMiddlwareExecuted = true;
builtAgent = builder.Use(
(_, _, _, _, _) =>
{
Assert.NotNull(AIAgent.CurrentRunContext);
Assert.Same(builtAgent, AIAgent.CurrentRunContext.Agent);
Assert.Same(mockSession.Object, AIAgent.CurrentRunContext.Session);
nonStreamingMiddlewareExecuted = true;
return Task.FromResult(new AgentResponse());
},
(_, _, _, _, _) =>
{
Assert.NotNull(AIAgent.CurrentRunContext);
Assert.Same(builtAgent, AIAgent.CurrentRunContext.Agent);
Assert.Same(mockSession.Object, AIAgent.CurrentRunContext.Session);
streamingMiddlwareExecuted = true;
return AsyncEnumerable.Empty<AgentResponseUpdate>();
}).Build();
// Act
await builtAgent.RunAsync("Input message", mockSession.Object);
await foreach (var update in builtAgent.RunStreamingAsync("Input message", mockSession.Object))
{
}
// Assert
Assert.True(nonStreamingMiddlewareExecuted);
Assert.True(streamingMiddlwareExecuted);
}
#endregion
/// <summary>
@@ -382,10 +382,13 @@ public class AgentExtensionsTests
this._exceptionToThrow = exceptionToThrow;
}
public override ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
protected override JsonElement SerializeSessionCore(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override string? Name { get; }
@@ -327,4 +327,9 @@ public class ChatClientAgentSessionTests
}
#endregion
internal sealed class Animal
{
public string Name { get; set; } = string.Empty;
}
}

Some files were not shown because too many files have changed in this diff Show More