Merge branch 'main' into feature-session-statebag

This commit is contained in:
westey
2026-02-10 20:39:26 +00:00
committed by GitHub
93 changed files with 2838 additions and 810 deletions
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a local MCP (Model Context Protocol) client with Azure Foundry Agents.
// The MCP tools are resolved locally by connecting directly to the MCP server via HTTP,
// and then passed to the Foundry agent as client-side tools.
// This sample uses the Microsoft Learn MCP endpoint to search documentation.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string AgentInstructions = "You are a helpful assistant that can help with Microsoft documentation questions. Use the Microsoft Learn MCP tool to search for documentation.";
const string AgentName = "DocsAgent";
// Connect to the MCP server locally via HTTP (Streamable HTTP transport).
// The MCP server is hosted at Microsoft Learn and provides documentation search capabilities.
Console.WriteLine("Connecting to MCP server at https://learn.microsoft.com/api/mcp ...");
await using McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
{
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
Name = "Microsoft Learn MCP",
}));
// Retrieve the list of tools available on the MCP server (resolved locally).
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");
// Wrap each MCP tool with a DelegatingAIFunction to log local invocations.
List<AITool> wrappedTools = mcpTools.Select(tool => (AITool)new LoggingMcpTool(tool)).ToList();
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
// Create the agent with the locally-resolved MCP tools.
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
name: AgentName,
instructions: AgentInstructions,
tools: wrappedTools);
Console.WriteLine($"Agent '{agent.Name}' created successfully.");
try
{
// First query
const string Prompt1 = "How does one create an Azure storage account using az cli?";
Console.WriteLine($"\nUser: {Prompt1}\n");
AgentResponse response1 = await agent.RunAsync(Prompt1);
Console.WriteLine($"Agent: {response1}");
Console.WriteLine("\n=======================================\n");
// Second query
const string Prompt2 = "What is Microsoft Agent Framework?";
Console.WriteLine($"User: {Prompt2}\n");
AgentResponse response2 = await agent.RunAsync(Prompt2);
Console.WriteLine($"Agent: {response2}");
}
finally
{
// Cleanup by removing the agent when done
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
Console.WriteLine($"\nAgent '{agent.Name}' deleted.");
}
/// <summary>
/// Wraps an MCP tool to log when it is invoked locally,
/// confirming that the MCP call is happening client-side.
/// </summary>
internal sealed class LoggingMcpTool(AIFunction innerFunction) : DelegatingAIFunction(innerFunction)
{
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
{
Console.WriteLine($" >> [LOCAL MCP] Invoking tool '{this.Name}' locally...");
return base.InvokeCoreAsync(arguments, cancellationToken);
}
}
@@ -0,0 +1,48 @@
# Using Local MCP Client with Azure Foundry Agents
This sample demonstrates how to use a local MCP (Model Context Protocol) client with Azure Foundry Agents. Unlike the hosted MCP approach where Azure Foundry invokes the MCP server on the service side, this sample connects to the MCP server directly from the client via HTTP (Streamable HTTP transport) and passes the resolved tools to the agent.
## What this sample demonstrates
- Connecting to an MCP server locally using `HttpClientTransport`
- Discovering available tools from the MCP server client-side
- Passing locally-resolved MCP tools to a Foundry agent
- Using the Microsoft Learn MCP endpoint for documentation search
- Managing agent lifecycle (creation and deletion)
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- Azure Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
Set the following environment variables:
```powershell
$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Run the sample
Navigate to the FoundryAgents sample directory and run:
```powershell
cd dotnet/samples/GettingStarted/FoundryAgents
dotnet run --project .\FoundryAgents_Step27_LocalMCP
```
## Expected behavior
The sample will:
1. Connect to the Microsoft Learn MCP server via HTTP and list available tools
2. Create an agent with the locally-resolved MCP tools
3. Ask two questions about Microsoft documentation
4. The agent will use the MCP tools (invoked locally) to search Microsoft Learn documentation
5. Display the agent's responses with information from the documentation
6. Clean up resources by deleting the agent
@@ -58,6 +58,7 @@ Before you begin, ensure you have the following prerequisites:
|[Using plugins](./FoundryAgents_Step13_Plugins/)|This sample demonstrates how to use plugins with a Foundry agent|
|[Code interpreter](./FoundryAgents_Step14_CodeInterpreter/)|This sample demonstrates how to use the code interpreter tool with a Foundry agent|
|[Computer use](./FoundryAgents_Step15_ComputerUse/)|This sample demonstrates how to use computer use capabilities with a Foundry agent|
|[Local MCP](./FoundryAgents_Step27_LocalMCP/)|This sample demonstrates how to use a local MCP client with a Foundry agent|
## Running the samples from the console
@@ -31,23 +31,23 @@ namespace Microsoft.Agents.AI;
/// </remarks>
public abstract class AIContextProvider
{
private readonly string _sourceName;
private readonly string _sourceId;
/// <summary>
/// Initializes a new instance of the <see cref="AIContextProvider"/> class.
/// </summary>
protected AIContextProvider()
{
this._sourceName = this.GetType().FullName!;
this._sourceId = this.GetType().FullName!;
}
/// <summary>
/// Initializes a new instance of the <see cref="AIContextProvider"/> class with the specified source name.
/// Initializes a new instance of the <see cref="AIContextProvider"/> class with the specified source id.
/// </summary>
/// <param name="sourceName">The source name to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="AIContextProvider"/>.</param>
protected AIContextProvider(string sourceName)
/// <param name="sourceId">The source id to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="AIContextProvider"/>.</param>
protected AIContextProvider(string sourceId)
{
this._sourceName = sourceName;
this._sourceId = sourceId;
}
/// <summary>
@@ -85,27 +85,9 @@ public abstract class AIContextProvider
return aiContext;
}
aiContext.Messages = aiContext.Messages.Select(message =>
{
if (message.AdditionalProperties != null
// Check if the message was already tagged with this provider's source type
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var messageSourceType)
&& messageSourceType is AgentRequestMessageSourceType typedMessageSourceType
&& typedMessageSourceType == AgentRequestMessageSourceType.AIContextProvider
// Check if the message was already tagged with this provider's source
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var messageSource)
&& messageSource is string typedMessageSource
&& typedMessageSource == this._sourceName)
{
return message;
}
message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.AIContextProvider;
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = this._sourceName;
return message;
}).ToList();
aiContext.Messages = aiContext.Messages
.Select(message => message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.AIContextProvider, this._sourceId))
.ToList();
return aiContext;
}
@@ -1,16 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides a constant for the key used to store the source of the agent request message.
/// </summary>
public static class AgentRequestMessageSource
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the source of the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "Agent.RequestMessageSource";
}
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents attribution information for the source of an agent request message for a specific run, including the component type and
/// identifier.
/// </summary>
/// <remarks>
/// Use this struct to identify which component provided a message during an agent run.
/// This is useful to allow filtering of messages based on their source, such as distinguishing between user input, middleware-generated messages, and chat history.
/// </remarks>
public readonly struct AgentRequestMessageSourceAttribution : IEquatable<AgentRequestMessageSourceAttribution>
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the <see cref="AgentRequestMessageSourceAttribution"/>
/// associated with the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "_attribution";
/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceAttribution"/> struct with the specified source type and identifier.
/// </summary>
/// <param name="sourceType">The <see cref="AgentRequestMessageSourceType"/> of the component that provided the message.</param>
/// <param name="sourceId">The unique identifier of the component that provided the message.</param>
public AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType sourceType, string? sourceId)
{
this.SourceType = sourceType;
this.SourceId = sourceId;
}
/// <summary>
/// Gets the type of component that provided the message for the current agent run.
/// </summary>
public AgentRequestMessageSourceType SourceType { get; }
/// <summary>
/// Gets the unique identifier of the component that provided the message for the current agent run.
/// </summary>
public string? SourceId { get; }
/// <summary>
/// Determines whether the specified <see cref="AgentRequestMessageSourceAttribution"/> is equal to the current instance.
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSourceAttribution"/> to compare with the current instance.</param>
/// <returns><see langword="true"/> if the specified instance is equal to the current instance; otherwise, <see langword="false"/>.</returns>
public bool Equals(AgentRequestMessageSourceAttribution other)
{
return this.SourceType == other.SourceType &&
string.Equals(this.SourceId, other.SourceId, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether the specified object is equal to the current instance.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns><see langword="true"/> if the specified object is equal to the current instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj)
{
return obj is AgentRequestMessageSourceAttribution other && this.Equals(other);
}
/// <summary>
/// Returns a hash code for the current instance.
/// </summary>
/// <returns>A hash code for the current instance.</returns>
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = (hash * 31) + this.SourceType.GetHashCode();
hash = (hash * 31) + (this.SourceId?.GetHashCode() ?? 0);
return hash;
}
}
/// <summary>
/// Determines whether two <see cref="AgentRequestMessageSourceAttribution"/> instances are equal.
/// </summary>
/// <param name="left">The first instance to compare.</param>
/// <param name="right">The second instance to compare.</param>
/// <returns><see langword="true"/> if the instances are equal; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(AgentRequestMessageSourceAttribution left, AgentRequestMessageSourceAttribution right)
{
return left.Equals(right);
}
/// <summary>
/// Determines whether two <see cref="AgentRequestMessageSourceAttribution"/> instances are not equal.
/// </summary>
/// <param name="left">The first instance to compare.</param>
/// <param name="right">The second instance to compare.</param>
/// <returns><see langword="true"/> if the instances are not equal; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(AgentRequestMessageSourceAttribution left, AgentRequestMessageSourceAttribution right)
{
return !left.Equals(right);
}
}
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
@@ -14,15 +13,10 @@ namespace Microsoft.Agents.AI;
/// This type helps to identify whether a message came from outside the agent pipeline,
/// whether it was produced by middleware, or came from chat history.
/// </remarks>
public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessageSourceType>
public readonly struct AgentRequestMessageSourceType : IEquatable<AgentRequestMessageSourceType>
{
/// <summary>
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the source type of the agent request message.
/// </summary>
public static readonly string AdditionalPropertiesKey = "Agent.RequestMessageSourceType";
/// <summary>
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceType"/> class.
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceType"/> struct.
/// </summary>
/// <param name="value">The string value representing the source of the agent request message.</param>
public AgentRequestMessageSourceType(string value) => this.Value = Throw.IfNullOrWhitespace(value);
@@ -30,7 +24,7 @@ public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessa
/// <summary>
/// Get the string value representing the source of the agent request message.
/// </summary>
public string Value { get; }
public string Value { get { return field ?? External.Value; } }
/// <summary>
/// The message came from outside the agent pipeline (e.g., user input).
@@ -52,18 +46,8 @@ public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessa
/// </summary>
/// <param name="other">The <see cref="AgentRequestMessageSourceType"/> to compare to this instance.</param>
/// <returns><see langword="true"/> if the value of the <paramref name="other"/> parameter is the same as the value of this instance; otherwise, <see langword="false"/>.</returns>
public bool Equals(AgentRequestMessageSourceType? other)
public bool Equals(AgentRequestMessageSourceType other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return string.Equals(this.Value, other.Value, StringComparison.Ordinal);
}
@@ -72,7 +56,7 @@ public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessa
/// </summary>
/// <param name="obj">The object to compare to this instance.</param>
/// <returns><see langword="true"/> if <paramref name="obj"/> is a <see cref="AgentRequestMessageSourceType"/> and its value is the same as this instance; otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj) => this.Equals(obj as AgentRequestMessageSourceType);
public override bool Equals(object? obj) => obj is AgentRequestMessageSourceType other && this.Equals(other);
/// <summary>
/// Returns the hash code for this instance.
@@ -86,13 +70,8 @@ public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessa
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is the same as the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(AgentRequestMessageSourceType? left, AgentRequestMessageSourceType? right)
public static bool operator ==(AgentRequestMessageSourceType left, AgentRequestMessageSourceType right)
{
if (left is null)
{
return right is null;
}
return left.Equals(right);
}
@@ -102,5 +81,5 @@ public sealed class AgentRequestMessageSourceType : IEquatable<AgentRequestMessa
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is different from the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(AgentRequestMessageSourceType? left, AgentRequestMessageSourceType? right) => !(left == right);
public static bool operator !=(AgentRequestMessageSourceType left, AgentRequestMessageSourceType right) => !(left == right);
}
@@ -40,23 +40,23 @@ namespace Microsoft.Agents.AI;
/// </remarks>
public abstract class ChatHistoryProvider
{
private readonly string _sourceName;
private readonly string _sourceId;
/// <summary>
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class.
/// </summary>
protected ChatHistoryProvider()
{
this._sourceName = this.GetType().FullName!;
this._sourceId = this.GetType().FullName!;
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class with the specified source name.
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class with the specified source id.
/// </summary>
/// <param name="sourceName">The source name to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="ChatHistoryProvider"/>.</param>
protected ChatHistoryProvider(string sourceName)
/// <param name="sourceId">The source id to stamp on <see cref="ChatMessage.AdditionalProperties"/> for each messages produced by the <see cref="ChatHistoryProvider"/>.</param>
protected ChatHistoryProvider(string sourceId)
{
this._sourceName = sourceName;
this._sourceId = sourceId;
}
/// <summary>
@@ -98,27 +98,7 @@ public abstract class ChatHistoryProvider
{
var messages = await this.InvokingCoreAsync(context, cancellationToken).ConfigureAwait(false);
return messages.Select(message =>
{
if (message.AdditionalProperties != null
// Check if the message was already tagged with this provider's source type
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var messageSourceType)
&& messageSourceType is AgentRequestMessageSourceType typedMessageSourceType
&& typedMessageSourceType == AgentRequestMessageSourceType.ChatHistory
// Check if the message was already tagged with this provider's source
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out var messageSource)
&& messageSource is string typedMessageSource
&& typedMessageSource == this._sourceName)
{
return message;
}
message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.ChatHistory;
message.AdditionalProperties[AgentRequestMessageSource.AdditionalPropertiesKey] = this._sourceName;
return message;
});
return messages.Select(message => message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.ChatHistory, this._sourceId));
}
/// <summary>
@@ -45,7 +45,7 @@ public static class ChatHistoryProviderExtensions
innerProvider: provider,
invokedMessagesFilter: (ctx) =>
{
ctx.RequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSource() != AgentRequestMessageSourceType.AIContextProvider);
ctx.RequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider);
return ctx;
});
}
@@ -10,18 +10,66 @@ namespace Microsoft.Agents.AI;
public static class ChatMessageExtensions
{
/// <summary>
/// Gets the source of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
/// Gets the source type of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
/// </summary>
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source.</param>
/// <returns>An <see cref="AgentRequestMessageSourceType"/> value indicating the source of the <see cref="ChatMessage"/>. Defaults to <see
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source type.</param>
/// <returns>An <see cref="AgentRequestMessageSourceType"/> value indicating the source type of the <see cref="ChatMessage"/>. Defaults to <see
/// cref="AgentRequestMessageSourceType.External"/> if no explicit source is defined.</returns>
public static AgentRequestMessageSourceType GetAgentRequestMessageSource(this ChatMessage message)
public static AgentRequestMessageSourceType GetAgentRequestMessageSourceType(this ChatMessage message)
{
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out var source) is true && source is AgentRequestMessageSourceType typedSource)
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var attribution) is true
&& attribution is AgentRequestMessageSourceAttribution typedAttribution)
{
return typedSource;
return typedAttribution.SourceType;
}
return AgentRequestMessageSourceType.External;
}
/// <summary>
/// Gets the source id of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
/// </summary>
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source id.</param>
/// <returns>An <see cref="string"/> value indicating the source id of the <see cref="ChatMessage"/>. Defaults to <see langword="null"/>
/// if no explicit source id is defined.</returns>
public static string? GetAgentRequestMessageSourceId(this ChatMessage message)
{
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var attribution) is true
&& attribution is AgentRequestMessageSourceAttribution typedAttribution)
{
return typedAttribution.SourceId;
}
return null;
}
/// <summary>
/// Ensure that the provided message is tagged with the provided source type and source id in the context of a specific agent run.
/// </summary>
/// <param name="message">The message to tag.</param>
/// <param name="sourceType">The source type to tag the message with.</param>
/// <param name="sourceId">The source id to tag the message with.</param>
/// <returns>The tagged message.</returns>
/// <remarks>
/// If the message is already tagged with the provided source type and source id, it is returned as is.
/// Otherwise, a cloned message is returned with the appropriate tagging in the AdditionalProperties.
/// </remarks>
public static ChatMessage AsAgentRequestMessageSourcedMessage(this ChatMessage message, AgentRequestMessageSourceType sourceType, string? sourceId = null)
{
if (message.AdditionalProperties != null
// Check if the message was already tagged with the required source type and source id
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var messageSourceAttribution)
&& messageSourceAttribution is AgentRequestMessageSourceAttribution typedMessageSourceAttribution
&& typedMessageSourceAttribution.SourceType == sourceType
&& typedMessageSourceAttribution.SourceId == sourceId)
{
return message;
}
message = message.Clone();
message.AdditionalProperties ??= new();
message.AdditionalProperties[AgentRequestMessageSourceAttribution.AdditionalPropertiesKey] =
new AgentRequestMessageSourceAttribution(sourceType, sourceId);
return message;
}
}
@@ -114,7 +114,7 @@ public sealed class Mem0Provider : AIContextProvider
string queryText = string.Join(
Environment.NewLine,
context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
.Where(m => !string.IsNullOrWhiteSpace(m.Text))
.Select(m => m.Text));
@@ -197,7 +197,7 @@ public sealed class Mem0Provider : AIContextProvider
await this.PersistMessagesAsync(
storageScope,
context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
.Concat(context.ResponseMessages ?? []),
cancellationToken).ConfigureAwait(false);
}
@@ -21,12 +21,14 @@ internal abstract class ProcessContentMetadataBase : GraphDataTypeBase
/// <param name="identifier">The unique identifier for the content.</param>
/// <param name="isTruncated">Indicates if the content is truncated.</param>
/// <param name="name">The name of the content.</param>
protected ProcessContentMetadataBase(ContentBase content, string identifier, bool isTruncated, string name) : base(ProcessConversationMetadataDataType)
/// <param name="correlationId">The correlation ID for the content.</param>
protected ProcessContentMetadataBase(ContentBase content, string identifier, bool isTruncated, string name, string correlationId) : base(ProcessConversationMetadataDataType)
{
this.Identifier = identifier;
this.IsTruncated = isTruncated;
this.Content = content;
this.Name = name;
this.CorrelationId = correlationId;
}
/// <summary>
@@ -55,7 +57,7 @@ internal abstract class ProcessContentMetadataBase : GraphDataTypeBase
/// Identifier to group multiple contents.
/// </summary>
[JsonPropertyName("correlationId")]
public string? CorrelationId { get; set; }
public string CorrelationId { get; set; }
/// <summary>
/// Gets or sets the sequenceNumber.
@@ -15,7 +15,7 @@ internal sealed class ProcessConversationMetadata : ProcessContentMetadataBase
/// <summary>
/// Initializes a new instance of the <see cref="ProcessConversationMetadata"/> class.
/// </summary>
public ProcessConversationMetadata(ContentBase contentBase, string identifier, bool isTruncated, string name) : base(contentBase, identifier, isTruncated, name)
public ProcessConversationMetadata(ContentBase contentBase, string identifier, bool isTruncated, string name, string correlationId) : base(contentBase, identifier, isTruncated, name, correlationId)
{
this.DataType = ProcessConversationMetadataDataType;
}
@@ -14,7 +14,7 @@ internal sealed class ProcessFileMetadata : ProcessContentMetadataBase
/// <summary>
/// Initializes a new instance of the <see cref="ProcessFileMetadata"/> class.
/// </summary>
public ProcessFileMetadata(ContentBase contentBase, string identifier, bool isTruncated, string name) : base(contentBase, identifier, isTruncated, name)
public ProcessFileMetadata(ContentBase contentBase, string identifier, bool isTruncated, string name, string correlationId) : base(contentBase, identifier, isTruncated, name, correlationId)
{
this.DataType = ProcessFileMetadataDataType;
}
@@ -19,7 +19,7 @@ public class PurviewSettings
/// <param name="appName">The publicly visible name of the application.</param>
public PurviewSettings(string appName)
{
this.AppName = appName;
this.AppName = string.IsNullOrWhiteSpace(appName) ? throw new ArgumentException("AppName cannot be null or whitespace.", nameof(appName)) : appName;
}
/// <summary>
@@ -53,7 +53,7 @@ internal sealed class PurviewWrapper : IDisposable
}
}
return Guid.NewGuid().ToString();
return string.Empty;
}
/// <summary>
@@ -136,12 +136,15 @@ internal sealed class PurviewWrapper : IDisposable
/// <returns>The agent's response. This could be the response from the agent or a message indicating that Purview has blocked the prompt or response.</returns>
public async Task<AgentResponse> ProcessAgentContentAsync(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
string sessionId = GetSessionIdFromAgentSession(session, messages);
string? resolvedUserId = null;
string sessionId = string.Empty;
try
{
sessionId = GetSessionIdFromAgentSession(session, messages);
if (string.IsNullOrEmpty(sessionId))
{
sessionId = Guid.NewGuid().ToString();
}
(bool shouldBlockPrompt, resolvedUserId) = await this._scopedProcessor.ProcessMessagesAsync(messages, sessionId, Activity.UploadText, this._purviewSettings, null, cancellationToken).ConfigureAwait(false);
if (shouldBlockPrompt)
@@ -171,7 +174,19 @@ internal sealed class PurviewWrapper : IDisposable
try
{
(bool shouldBlockResponse, _) = await this._scopedProcessor.ProcessMessagesAsync(response.Messages, sessionId, Activity.UploadText, this._purviewSettings, resolvedUserId, cancellationToken).ConfigureAwait(false);
string sessionIdResponse = GetSessionIdFromAgentSession(session, messages);
if (string.IsNullOrEmpty(sessionIdResponse))
{
if (string.IsNullOrEmpty(sessionId))
{
sessionIdResponse = Guid.NewGuid().ToString();
}
else
{
sessionIdResponse = sessionId;
}
}
(bool shouldBlockResponse, _) = await this._scopedProcessor.ProcessMessagesAsync(response.Messages, sessionIdResponse, Activity.UploadText, this._purviewSettings, resolvedUserId, cancellationToken).ConfigureAwait(false);
if (shouldBlockResponse)
{
@@ -121,9 +121,10 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
{
string messageId = message.MessageId ?? Guid.NewGuid().ToString();
ContentBase content = new PurviewTextContent(message.Text);
ProcessConversationMetadata conversationmetadata = new(content, messageId, false, $"Agent Framework Message {messageId}")
string correlationId = (sessionId ?? Guid.NewGuid().ToString()) + "@AF";
ProcessConversationMetadata conversationMetadata = new(content, messageId, false, $"Agent Framework Message {messageId}", correlationId)
{
CorrelationId = sessionId ?? Guid.NewGuid().ToString()
SequenceNumber = DateTime.UtcNow.Ticks,
};
ActivityMetadata activityMetadata = new(activity);
PolicyLocation policyLocation;
@@ -162,7 +163,7 @@ internal sealed class ScopedContentProcessor : IScopedContentProcessor
OperatingSystemVersion = "Unknown"
}
};
ContentToProcess contentToProcess = new([conversationmetadata], activityMetadata, deviceMetadata, integratedAppMetadata, protectedAppMetadata);
ContentToProcess contentToProcess = new([conversationMetadata], activityMetadata, deviceMetadata, integratedAppMetadata, protectedAppMetadata);
if (userId == null &&
tokenInfo?.UserId != null)
@@ -13,7 +13,13 @@ internal sealed class JsonMarshaller : IWireMarshaller<JsonElement>
public JsonMarshaller(JsonSerializerOptions? serializerOptions = null)
{
this._internalOptions = new JsonSerializerOptions(WorkflowsJsonUtilities.DefaultOptions);
this._internalOptions = new JsonSerializerOptions(WorkflowsJsonUtilities.DefaultOptions)
{
// Propagate from the user-provided options if set; enables support for databases
// like PostgreSQL jsonb that do not preserve property order.
AllowOutOfOrderMetadataProperties = serializerOptions?.AllowOutOfOrderMetadataProperties is true,
};
this._internalOptions.Converters.Add(new PortableValueConverter(this));
this._internalOptions.Converters.Add(new ExecutorIdentityConverter());
this._internalOptions.Converters.Add(new ScopeKeyConverter());
@@ -239,7 +239,7 @@ public sealed partial class ChatClientAgent : AIAgent
try
{
// Using the enumerator to ensure we consider the case where no updates are returned for notification.
responseUpdatesEnumerator = chatClient.GetStreamingResponseAsync(inputMessagesForProviders, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
responseUpdatesEnumerator = chatClient.GetStreamingResponseAsync(inputMessagesForChatClient, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (Exception ex)
{
@@ -166,7 +166,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
{
// Get the text from the current request messages
var requestText = string.Join("\n", context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
.Where(m => m != null && !string.IsNullOrWhiteSpace(m.Text))
.Select(m => m.Text));
@@ -225,7 +225,7 @@ public sealed class ChatHistoryMemoryProvider : AIContextProvider, IDisposable
var collection = await this.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
List<Dictionary<string, object?>> itemsToStore = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
.Concat(context.ResponseMessages ?? [])
.Select(message => new Dictionary<string, object?>
{
@@ -102,7 +102,7 @@ public sealed class TextSearchProvider : AIContextProvider
// Aggregate text from memory + current request messages.
var sbInput = new StringBuilder();
var requestMessagesText = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
.Where(x => !string.IsNullOrWhiteSpace(x?.Text)).Select(x => x.Text);
foreach (var messageText in recentMessagesText.Concat(requestMessagesText))
{
@@ -175,7 +175,7 @@ public sealed class TextSearchProvider : AIContextProvider
?? [];
var newMessagesText = context.RequestMessages
.Where(m => m.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External)
.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
.Concat(context.ResponseMessages ?? [])
.Where(m =>
this._recentMessageRolesIncluded.Contains(m.Role) &&
@@ -19,7 +19,7 @@ public class AIContextProviderTests
#region InvokingAsync Message Stamping Tests
[Fact]
public async Task InvokingAsync_StampsMessagesWithSourceTypeAndSourceAsync()
public async Task InvokingAsync_StampsMessagesWithSourceTypeAndSourceIdAsync()
{
// Arrange
var provider = new TestAIContextProviderWithMessages();
@@ -32,18 +32,18 @@ public class AIContextProviderTests
Assert.NotNull(aiContext.Messages);
ChatMessage message = aiContext.Messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestAIContextProviderWithMessages).FullName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, typedAttribution.SourceType);
Assert.Equal(typeof(TestAIContextProviderWithMessages).FullName, typedAttribution.SourceId);
}
[Fact]
public async Task InvokingAsync_WithCustomSourceName_StampsMessagesWithCustomSourceAsync()
public async Task InvokingAsync_WithCustomSourceId_StampsMessagesWithCustomSourceIdAsync()
{
// Arrange
const string CustomSourceName = "CustomContextSource";
var provider = new TestAIContextProviderWithCustomSource(CustomSourceName);
const string CustomSourceId = "CustomContextSource";
var provider = new TestAIContextProviderWithCustomSource(CustomSourceId);
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
@@ -53,10 +53,10 @@ public class AIContextProviderTests
Assert.NotNull(aiContext.Messages);
ChatMessage message = aiContext.Messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(CustomSourceName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, typedAttribution.SourceType);
Assert.Equal(CustomSourceId, typedAttribution.SourceId);
}
[Fact]
@@ -73,10 +73,10 @@ public class AIContextProviderTests
Assert.NotNull(aiContext.Messages);
ChatMessage message = aiContext.Messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestAIContextProviderWithPreStampedMessages).FullName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, typedAttribution.SourceType);
Assert.Equal(typeof(TestAIContextProviderWithPreStampedMessages).FullName, typedAttribution.SourceId);
}
[Fact]
@@ -97,10 +97,10 @@ public class AIContextProviderTests
foreach (ChatMessage message in messageList)
{
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestAIContextProviderWithMultipleMessages).FullName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, typedAttribution.SourceType);
Assert.Equal(typeof(TestAIContextProviderWithMultipleMessages).FullName, typedAttribution.SourceId);
}
}
@@ -473,7 +473,7 @@ public class AIContextProviderTests
private sealed class TestAIContextProviderWithCustomSource : AIContextProvider
{
public TestAIContextProviderWithCustomSource(string sourceName) : base(sourceName)
public TestAIContextProviderWithCustomSource(string sourceId) : base(sourceId)
{
}
@@ -491,8 +491,7 @@ public class AIContextProviderTests
var message = new ChatMessage(ChatRole.System, "Pre-stamped Message");
message.AdditionalProperties = new AdditionalPropertiesDictionary
{
[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.AIContextProvider,
[AgentRequestMessageSource.AdditionalPropertiesKey] = this.GetType().FullName!
[AgentRequestMessageSourceAttribution.AdditionalPropertiesKey] = new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!)
};
return new(new AIContext
{
@@ -0,0 +1,466 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AgentRequestMessageSourceAttribution"/> struct.
/// </summary>
public sealed class AgentRequestMessageSourceAttributionTests
{
#region Constructor Tests
[Fact]
public void Constructor_SetsSourceTypeAndSourceId()
{
// Arrange
AgentRequestMessageSourceType expectedType = AgentRequestMessageSourceType.AIContextProvider;
const string ExpectedId = "MyProvider";
// Act
AgentRequestMessageSourceAttribution attribution = new(expectedType, ExpectedId);
// Assert
Assert.Equal(expectedType, attribution.SourceType);
Assert.Equal(ExpectedId, attribution.SourceId);
}
[Fact]
public void Constructor_WithNullSourceId_SetsNullSourceId()
{
// Arrange
AgentRequestMessageSourceType sourceType = AgentRequestMessageSourceType.ChatHistory;
// Act
AgentRequestMessageSourceAttribution attribution = new(sourceType, null);
// Assert
Assert.Equal(sourceType, attribution.SourceType);
Assert.Null(attribution.SourceId);
}
#endregion
#region AdditionalPropertiesKey Tests
[Fact]
public void AdditionalPropertiesKey_IsAttribution()
{
// Assert
Assert.Equal("_attribution", AgentRequestMessageSourceAttribution.AdditionalPropertiesKey);
}
#endregion
#region Default Value Tests
[Fact]
public void Default_HasDefaultSourceTypeAndNullSourceId()
{
// Arrange & Act
AgentRequestMessageSourceAttribution attribution = default;
// Assert
Assert.Equal(default, attribution.SourceType);
Assert.Null(attribution.SourceId);
}
#endregion
#region Equals (IEquatable) Tests
[Fact]
public void Equals_WithSameSourceTypeAndSourceId_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithDifferentSourceType_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider1");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentSourceTypeAndSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentCaseSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "provider");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_BothDefaultValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = default;
AgentRequestMessageSourceAttribution attribution2 = default;
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithBothNullSourceIds_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.External, null!);
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, null!);
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithOneNullSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.External, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, null!);
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
#endregion
#region Object.Equals Tests
[Fact]
public void ObjectEquals_WithEqualAttribution_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
object attribution2 = new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "Provider");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void ObjectEquals_WithDifferentType_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
object other = "NotAnAttribution";
// Act
bool result = attribution.Equals(other);
// Assert
Assert.False(result);
}
[Fact]
public void ObjectEquals_WithNullObject_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
object? other = null;
// Act
bool result = attribution.Equals(other);
// Assert
Assert.False(result);
}
[Fact]
public void ObjectEquals_WithBoxedDifferentAttribution_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.ChatHistory, "Provider1");
object attribution2 = new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
#endregion
#region GetHashCode Tests
[Fact]
public void GetHashCode_WithSameValues_ReturnsSameHashCode()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
// Act
int hashCode1 = attribution1.GetHashCode();
int hashCode2 = attribution2.GetHashCode();
// Assert
Assert.Equal(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_WithDifferentSourceType_ReturnsDifferentHashCode()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
// Act
int hashCode1 = attribution1.GetHashCode();
int hashCode2 = attribution2.GetHashCode();
// Assert
Assert.NotEqual(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_WithDifferentSourceId_ReturnsDifferentHashCode()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
int hashCode1 = attribution1.GetHashCode();
int hashCode2 = attribution2.GetHashCode();
// Assert
Assert.NotEqual(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_ConsistentWithEquals()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.External, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, "Provider");
// Act & Assert
Assert.True(attribution1.Equals(attribution2));
Assert.Equal(attribution1.GetHashCode(), attribution2.GetHashCode());
}
[Fact]
public void GetHashCode_WithNullSourceId_DoesNotThrow()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.External, null!);
// Act
int hashCode = attribution.GetHashCode();
// Assert
Assert.IsType<int>(hashCode);
}
#endregion
#region Equality Operator Tests
[Fact]
public void EqualityOperator_WithEqualValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithDifferentValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithBothDefault_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = default;
AgentRequestMessageSourceAttribution attribution2 = default;
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithDifferentSourceTypeOnly_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, "Provider");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithDifferentSourceIdOnly_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.False(result);
}
#endregion
#region Inequality Operator Tests
[Fact]
public void InequalityOperator_WithEqualValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithDifferentValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithBothDefault_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = default;
AgentRequestMessageSourceAttribution attribution2 = default;
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithDifferentSourceTypeOnly_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, "Provider");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithDifferentSourceIdOnly_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.True(result);
}
#endregion
}
@@ -5,7 +5,7 @@ using System;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AgentRequestMessageSourceType"/> class.
/// Contains tests for the <see cref="AgentRequestMessageSourceType"/> struct.
/// </summary>
public sealed class AgentRequestMessageSourceTypeTests
{
@@ -38,6 +38,16 @@ public sealed class AgentRequestMessageSourceTypeTests
Assert.Throws<ArgumentException>(() => new AgentRequestMessageSourceType(string.Empty));
}
[Fact]
public void Default_DefaultsToExternal()
{
// Act
AgentRequestMessageSourceType defaultSource = default;
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, defaultSource);
}
#endregion
#region Static Properties Tests
@@ -49,7 +59,6 @@ public sealed class AgentRequestMessageSourceTypeTests
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.External;
// Assert
Assert.NotNull(source);
Assert.Equal("External", source.Value);
}
@@ -60,7 +69,6 @@ public sealed class AgentRequestMessageSourceTypeTests
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.AIContextProvider;
// Assert
Assert.NotNull(source);
Assert.Equal("AIContextProvider", source.Value);
}
@@ -71,22 +79,11 @@ public sealed class AgentRequestMessageSourceTypeTests
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.ChatHistory;
// Assert
Assert.NotNull(source);
Assert.Equal("ChatHistory", source.Value);
}
[Fact]
public void AdditionalPropertiesKey_ReturnsExpectedValue()
{
// Arrange & Act
string key = AgentRequestMessageSourceType.AdditionalPropertiesKey;
// Assert
Assert.Equal("Agent.RequestMessageSourceType", key);
}
[Fact]
public void StaticProperties_ReturnSameInstanceOnMultipleCalls()
public void StaticProperties_ReturnEqualValuesOnMultipleCalls()
{
// Arrange & Act
AgentRequestMessageSourceType external1 = AgentRequestMessageSourceType.External;
@@ -97,9 +94,9 @@ public sealed class AgentRequestMessageSourceTypeTests
AgentRequestMessageSourceType chatHistory2 = AgentRequestMessageSourceType.ChatHistory;
// Assert
Assert.Same(external1, external2);
Assert.Same(aiContextProvider1, aiContextProvider2);
Assert.Same(chatHistory1, chatHistory2);
Assert.Equal(external1, external2);
Assert.Equal(aiContextProvider1, aiContextProvider2);
Assert.Equal(chatHistory1, chatHistory2);
}
#endregion
@@ -148,7 +145,7 @@ public sealed class AgentRequestMessageSourceTypeTests
}
[Fact]
public void Equals_WithNull_ReturnsFalse()
public void Equals_WithNullObject_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
@@ -314,11 +311,11 @@ public sealed class AgentRequestMessageSourceTypeTests
}
[Fact]
public void EqualityOperator_WithBothNull_ReturnsTrue()
public void EqualityOperator_WithDefaultValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType? source2 = null;
AgentRequestMessageSourceType source1 = default;
AgentRequestMessageSourceType source2 = default;
// Act
bool result = source1 == source2;
@@ -327,34 +324,6 @@ public sealed class AgentRequestMessageSourceTypeTests
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithLeftNull_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 == source2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithRightNull_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType? source2 = null;
// Act
bool result = source1 == source2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithStaticInstances_ReturnsTrue()
{
@@ -416,11 +385,11 @@ public sealed class AgentRequestMessageSourceTypeTests
}
[Fact]
public void InequalityOperator_WithBothNull_ReturnsFalse()
public void InequalityOperator_WithBothDefault_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType? source2 = null;
AgentRequestMessageSourceType source1 = default;
AgentRequestMessageSourceType source2 = default;
// Act
bool result = source1 != source2;
@@ -429,34 +398,6 @@ public sealed class AgentRequestMessageSourceTypeTests
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithLeftNull_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType? source1 = null;
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 != source2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithRightNull_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType? source2 = null;
// Act
bool result = source1 != source2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_DifferentStaticInstances_ReturnsTrue()
{
@@ -64,7 +64,7 @@ public sealed class ChatHistoryProviderExtensionsTests
Mock<ChatHistoryProvider> providerMock = new();
List<ChatMessage> requestMessages =
[
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
new(ChatRole.User, "Hello")
];
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages)
@@ -114,9 +114,9 @@ public sealed class ChatHistoryProviderExtensionsTests
Mock<ChatHistoryProvider> providerMock = new();
List<ChatMessage> requestMessages =
[
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
new(ChatRole.User, "Hello"),
new(ChatRole.System, "Context") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.AIContextProvider } } }
new(ChatRole.System, "Context") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "TestContextSource") } } }
];
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages);
@@ -170,7 +170,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
var innerProviderMock = new Mock<ChatHistoryProvider>();
List<ChatMessage> requestMessages =
[
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
new(ChatRole.User, "Hello"),
];
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response") };
@@ -189,7 +189,7 @@ public sealed class ChatHistoryProviderMessageFilterTests
// Filter that modifies the context
ChatHistoryProvider.InvokedContext InvokedFilter(ChatHistoryProvider.InvokedContext ctx)
{
var modifiedRequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSource() == AgentRequestMessageSourceType.External).Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
var modifiedRequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External).Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
return new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, modifiedRequestMessages)
{
ResponseMessages = ctx.ResponseMessages,
@@ -21,7 +21,7 @@ public class ChatHistoryProviderTests
#region InvokingAsync Message Stamping Tests
[Fact]
public async Task InvokingAsync_StampsMessagesWithSourceTypeAndSourceAsync()
public async Task InvokingAsync_StampsMessagesWithSourceTypeAndSourceIdAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
@@ -33,18 +33,18 @@ public class ChatHistoryProviderTests
// Assert
ChatMessage message = messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestChatHistoryProvider).FullName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, typedAttribution.SourceType);
Assert.Equal(typeof(TestChatHistoryProvider).FullName, typedAttribution.SourceId);
}
[Fact]
public async Task InvokingAsync_WithCustomSourceName_StampsMessagesWithCustomSourceAsync()
public async Task InvokingAsync_WithCustomSourceId_StampsMessagesWithCustomSourceIdAsync()
{
// Arrange
const string CustomSourceName = "CustomHistorySource";
var provider = new TestChatHistoryProviderWithCustomSource(CustomSourceName);
const string CustomSourceId = "CustomHistorySource";
var provider = new TestChatHistoryProviderWithCustomSource(CustomSourceId);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Request")]);
// Act
@@ -53,10 +53,10 @@ public class ChatHistoryProviderTests
// Assert
ChatMessage message = messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(CustomSourceName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, typedAttribution.SourceType);
Assert.Equal(CustomSourceId, typedAttribution.SourceId);
}
[Fact]
@@ -72,10 +72,10 @@ public class ChatHistoryProviderTests
// Assert
ChatMessage message = messages.Single();
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestChatHistoryProviderWithPreStampedMessages).FullName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, typedAttribution.SourceType);
Assert.Equal(typeof(TestChatHistoryProviderWithPreStampedMessages).FullName, typedAttribution.SourceId);
}
[Fact]
@@ -95,10 +95,10 @@ public class ChatHistoryProviderTests
foreach (ChatMessage message in messageList)
{
Assert.NotNull(message.AdditionalProperties);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceType.AdditionalPropertiesKey, out object? sourceType));
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, sourceType);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSource.AdditionalPropertiesKey, out object? source));
Assert.Equal(typeof(TestChatHistoryProviderWithMultipleMessages).FullName, source);
Assert.True(message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out object? attribution));
var typedAttribution = Assert.IsType<AgentRequestMessageSourceAttribution>(attribution);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, typedAttribution.SourceType);
Assert.Equal(typeof(TestChatHistoryProviderWithMultipleMessages).FullName, typedAttribution.SourceId);
}
}
@@ -379,7 +379,7 @@ public class ChatHistoryProviderTests
private sealed class TestChatHistoryProviderWithCustomSource : ChatHistoryProvider
{
public TestChatHistoryProviderWithCustomSource(string sourceName) : base(sourceName)
public TestChatHistoryProviderWithCustomSource(string sourceId) : base(sourceId)
{
}
@@ -397,8 +397,7 @@ public class ChatHistoryProviderTests
var message = new ChatMessage(ChatRole.User, "Pre-stamped Message");
message.AdditionalProperties = new AdditionalPropertiesDictionary
{
[AgentRequestMessageSourceType.AdditionalPropertiesKey] = AgentRequestMessageSourceType.ChatHistory,
[AgentRequestMessageSource.AdditionalPropertiesKey] = this.GetType().FullName!
[AgentRequestMessageSourceAttribution.AdditionalPropertiesKey] = new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!)
};
return new([message]);
}
@@ -9,23 +9,23 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// </summary>
public sealed class ChatMessageExtensionsTests
{
#region GetAgentRequestMessageSource Tests
#region GetAgentRequestMessageSourceType Tests
[Fact]
public void GetAgentRequestMessageSource_WithNoAdditionalProperties_ReturnsExternal()
public void GetAgentRequestMessageSourceType_WithNoAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithNullAdditionalProperties_ReturnsExternal()
public void GetAgentRequestMessageSourceType_WithNullAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
@@ -34,14 +34,14 @@ public sealed class ChatMessageExtensionsTests
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithEmptyAdditionalProperties_ReturnsExternal()
public void GetAgentRequestMessageSourceType_WithEmptyAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
@@ -50,130 +50,130 @@ public sealed class ChatMessageExtensionsTests
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithExternalSource_ReturnsExternal()
public void GetAgentRequestMessageSourceType_WithExternalSourceType_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.External }
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithAIContextProviderSource_ReturnsAIContextProvider()
public void GetAgentRequestMessageSourceType_WithAIContextProviderSourceType_ReturnsAIContextProvider()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.AIContextProvider }
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithChatHistorySource_ReturnsChatHistory()
public void GetAgentRequestMessageSourceType_WithChatHistorySourceType_ReturnsChatHistory()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory }
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithCustomSource_ReturnsCustomSource()
public void GetAgentRequestMessageSourceType_WithCustomSourceType_ReturnsCustomSourceType()
{
// Arrange
AgentRequestMessageSourceType customSource = new("CustomSource");
AgentRequestMessageSourceType customSourceType = new("CustomSourceType");
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, customSource }
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(customSourceType, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(customSource, result);
Assert.Equal("CustomSource", result.Value);
Assert.Equal(customSourceType, result);
Assert.Equal("CustomSourceType", result.Value);
}
[Fact]
public void GetAgentRequestMessageSource_WithWrongKeyType_ReturnsExternal()
public void GetAgentRequestMessageSourceType_WithWrongAttributionType_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, "NotAnAgentRequestMessageSource" }
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, "NotAnAgentRequestMessageSourceAttribution" }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithNullValue_ReturnsExternal()
public void GetAgentRequestMessageSourceType_WithNullAttributionValue_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, null! }
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, null! }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSource_WithMultipleProperties_ReturnsCorrectSource()
public void GetAgentRequestMessageSourceType_WithMultipleProperties_ReturnsCorrectSourceType()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
@@ -181,17 +181,345 @@ public sealed class ChatMessageExtensionsTests
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "OtherProperty", "SomeValue" },
{ AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory },
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSourceId") },
{ "AnotherProperty", 123 }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSource();
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result);
}
#endregion
#region GetAgentRequestMessageSourceId Tests
[Fact]
public void GetAgentRequestMessageSourceId_WithNoAdditionalProperties_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithNullAdditionalProperties_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = null
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithEmptyAdditionalProperties_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary()
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithAttribution_ReturnsSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "MyProvider.FullName") }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Equal("MyProvider.FullName", result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithDifferentSourceIds_ReturnsCorrectSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "CustomHistorySourceId") }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Equal("CustomHistorySourceId", result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithWrongAttributionType_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, "NotAnAgentRequestMessageSourceAttribution" }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithNullAttributionValue_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, null! }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithMultipleProperties_ReturnsCorrectSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "OtherProperty", "SomeValue" },
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "ExpectedSourceId") },
{ "AnotherProperty", 123 }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Equal("ExpectedSourceId", result);
}
#endregion
#region AsAgentRequestMessageSourcedMessage Tests
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithNoAdditionalProperties_ReturnsClonesMessageWithAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.External, "TestSourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.External, result.GetAgentRequestMessageSourceType());
Assert.Equal("TestSourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithNullAdditionalProperties_ReturnsClonesMessageWithAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = null
};
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.AIContextProvider, "ProviderSourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result.GetAgentRequestMessageSourceType());
Assert.Equal("ProviderSourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithMatchingSourceTypeAndSourceId_ReturnsSameInstance()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistoryId") }
}
};
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.ChatHistory, "HistoryId");
// Assert
Assert.Same(message, result);
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithDifferentSourceType_ReturnsClonesMessageWithNewAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "SourceId") }
}
};
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.AIContextProvider, "SourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result.GetAgentRequestMessageSourceType());
Assert.Equal("SourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithDifferentSourceId_ReturnsClonesMessageWithNewAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "OriginalId") }
}
};
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.External, "NewId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.External, result.GetAgentRequestMessageSourceType());
Assert.Equal("NewId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithDefaultNullSourceId_ReturnsClonesMessageWithNullSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.ChatHistory);
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result.GetAgentRequestMessageSourceType());
Assert.Null(result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithMatchingSourceTypeAndNullSourceId_ReturnsSameInstance()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, null) }
}
};
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.External);
// Assert
Assert.Same(message, result);
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_DoesNotModifyOriginalMessage()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.AIContextProvider, "ProviderId");
// Assert
Assert.Null(message.AdditionalProperties);
Assert.NotNull(result.AdditionalProperties);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result.GetAgentRequestMessageSourceType());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithWrongAttributionType_ReturnsClonesMessageWithNewAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, "NotAnAttribution" }
}
};
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.External, "SourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.External, result.GetAgentRequestMessageSourceType());
Assert.Equal("SourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_PreservesMessageContent()
{
// Arrange
ChatMessage message = new(ChatRole.Assistant, "Test content");
// Act
ChatMessage result = message.AsAgentRequestMessageSourcedMessage(AgentRequestMessageSourceType.ChatHistory, "HistoryId");
// Assert
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Equal("Test content", result.Text);
}
#endregion
}
@@ -71,7 +71,7 @@ public class InMemoryChatHistoryProviderTests
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.System, "additional context") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.ChatHistory } } },
new(ChatRole.System, "additional context") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
};
var responseMessages = new List<ChatMessage>
{
@@ -302,7 +302,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
new ChatMessage(ChatRole.User, "First message"),
new ChatMessage(ChatRole.Assistant, "Second message"),
new ChatMessage(ChatRole.User, "Third message"),
new ChatMessage(ChatRole.System, "System context message") { AdditionalProperties = new() { { AgentRequestMessageSourceType.AdditionalPropertiesKey, AgentRequestMessageSourceType.AIContextProvider } } }
new ChatMessage(ChatRole.System, "System context message") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "TestSource") } } }
};
var responseMessages = new[]
{
@@ -478,7 +478,7 @@ public sealed class PurviewClientTests : IDisposable
private static ContentToProcess CreateValidContentToProcess()
{
var content = new PurviewTextContent("Test content");
var metadata = new ProcessConversationMetadata(content, "msg-123", false, "Test message");
var metadata = new ProcessConversationMetadata(content, "msg-123", false, "Test message", "test-correlation-id");
var activityMetadata = new ActivityMetadata(Activity.UploadText);
var deviceMetadata = new DeviceMetadata
{
@@ -1320,6 +1320,45 @@ public partial class ChatClientAgentTests
Assert.Equal("what?", historyMessages[1].Text);
}
/// <summary>
/// Verify that RunStreamingAsync includes chat history in messages sent to the chat client on subsequent calls.
/// </summary>
[Fact]
public async Task RunStreamingAsyncIncludesChatHistoryInMessagesToChatClientAsync()
{
// Arrange
List<IEnumerable<ChatMessage>> capturedMessages = [];
Mock<IChatClient> mockService = new();
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "response"),
];
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(returnUpdates))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((msgs, _, _) => capturedMessages.Add(msgs.ToList()));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
});
// Act
ChatClientAgentSession? session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await agent.RunStreamingAsync([new(ChatRole.User, "first")], session).ToListAsync();
await agent.RunStreamingAsync([new(ChatRole.User, "second")], session).ToListAsync();
// Assert - the second call should include chat history (first user message + first response) plus the new message
Assert.Equal(2, capturedMessages.Count);
var secondCallMessages = capturedMessages[1].ToList();
Assert.Equal(3, secondCallMessages.Count);
Assert.Equal("first", secondCallMessages[0].Text);
Assert.Equal("response", secondCallMessages[1].Text);
Assert.Equal("second", secondCallMessages[2].Text);
}
/// <summary>
/// Verify that RunStreamingAsync throws when a <see cref="ChatHistoryProvider"/> is provided and the chat client returns a conversation id.
/// </summary>
@@ -672,4 +672,104 @@ public class JsonSerializationTests
ValidateCheckpoint(retrievedCheckpoint, prototype);
}
/// <summary>
/// Verifies that the default behavior (without AllowOutOfOrderMetadataProperties) fails
/// when $type metadata is not the first property, demonstrating the PostgreSQL jsonb issue.
/// See: https://github.com/microsoft/agent-framework/issues/2962
/// </summary>
[Fact]
public void Test_OutOfOrderMetadataProperties_WithoutOption_Fails()
{
// Arrange
JsonMarshaller marshaller = new();
EdgeInfo edgeInfo = TestEdgeInfo_DirectNoCondition;
// Serialize to JSON
JsonElement serialized = marshaller.Marshal(edgeInfo);
string json = serialized.GetRawText();
// Simulate PostgreSQL jsonb behavior: reorder properties so $type is not first
string reorderedJson = ReorderJsonPropertiesToMoveTypeDiscriminatorLast(json);
// Act & Assert - Without the option, deserialization should fail
JsonElement reorderedElement = JsonDocument.Parse(reorderedJson).RootElement;
Action act = () => marshaller.Marshal<EdgeInfo>(reorderedElement);
act.Should().Throw<JsonException>();
}
/// <summary>
/// Simulates PostgreSQL jsonb behavior where property order is not preserved,
/// causing $type metadata to not be the first property.
/// This test verifies that deserialization works when AllowOutOfOrderMetadataProperties is enabled.
/// See: https://github.com/microsoft/agent-framework/issues/2962
/// </summary>
[Fact]
public void Test_OutOfOrderMetadataProperties_WithOptionEnabled_Succeeds()
{
// Arrange
EdgeInfo edgeInfo = TestEdgeInfo_DirectNoCondition;
// Serialize to JSON using standard marshaller
JsonMarshaller marshaller = new();
JsonElement serialized = marshaller.Marshal(edgeInfo);
string json = serialized.GetRawText();
// Simulate PostgreSQL jsonb behavior: reorder properties so $type is not first
string reorderedJson = ReorderJsonPropertiesToMoveTypeDiscriminatorLast(json);
JsonElement reorderedElement = JsonDocument.Parse(reorderedJson).RootElement;
// Act - Deserialize with AllowOutOfOrderMetadataProperties enabled via JsonSerializerOptions
JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonMarshaller marshallerWithOption = new(options);
EdgeInfo deserialized = marshallerWithOption.Marshal<EdgeInfo>(reorderedElement);
// Assert
deserialized.Should().Match(edgeInfo.CreatePolyValidator());
}
private static string ReorderJsonPropertiesToMoveTypeDiscriminatorLast(string json)
{
// Parse JSON, extract $type, rebuild with $type at end
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
Dictionary<string, JsonElement> properties = [];
JsonElement? typeValue = null;
foreach (JsonProperty prop in root.EnumerateObject())
{
if (prop.Name == "$type")
{
typeValue = prop.Value.Clone();
}
else
{
properties[prop.Name] = prop.Value.Clone();
}
}
// Rebuild JSON with $type last
using System.IO.MemoryStream ms = new();
using (Utf8JsonWriter writer = new(ms))
{
writer.WriteStartObject();
foreach (KeyValuePair<string, JsonElement> kvp in properties)
{
writer.WritePropertyName(kvp.Key);
kvp.Value.WriteTo(writer);
}
if (typeValue.HasValue)
{
writer.WritePropertyName("$type");
typeValue.Value.WriteTo(writer);
}
writer.WriteEndObject();
}
return System.Text.Encoding.UTF8.GetString(ms.ToArray());
}
}