Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ public IList<AIContent> Contents
/// <remarks>
/// If a <see cref="ChatMessage"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets any additional properties associated with the message.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,12 @@ public ResponseContinuationToken? ContinuationToken
/// <remarks>
/// If a <see cref="ChatResponse"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets any additional properties associated with the chat response.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,12 @@ public IList<AIContent> Contents
/// <remarks>
/// If a <see cref="ChatResponseUpdate"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets additional properties for the update.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ public AIAnnotation()
/// <remarks>
/// If an <see cref="AIAnnotation"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model, if needed.
/// for enabling a consumer to access the underlying object model, if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ public AIContent()
/// <remarks>
/// If an <see cref="AIContent"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model, if needed.
/// for enabling a consumer to access the underlying object model, if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets additional properties for the content.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ public ImageGenerationResponse(IList<AIContent>? contents)
/// <remarks>
/// If a <see cref="ImageGenerationResponse"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2818,6 +2818,30 @@
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.RawRepresentationJsonConverter : System.Text.Json.Serialization.JsonConverter<object?>",
"Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.RawRepresentationJsonConverter.RawRepresentationJsonConverter();",
"Stage": "Stable"
},
{
"Member": "override object? Microsoft.Extensions.AI.RawRepresentationJsonConverter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options);",
"Stage": "Stable"
},
{
"Member": "override void Microsoft.Extensions.AI.RawRepresentationJsonConverter.Write(System.Text.Json.Utf8JsonWriter writer, object? value, System.Text.Json.JsonSerializerOptions options);",
"Stage": "Stable"
}
],
"Properties": [
{
"Member": "override bool Microsoft.Extensions.AI.RawRepresentationJsonConverter.HandleNull { get; }",
"Stage": "Stable"
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.RequiredChatToolMode : Microsoft.Extensions.AI.ChatToolMode",
"Stage": "Stable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;
Expand All @@ -26,5 +27,7 @@ public class RealtimeClientMessage
/// The raw representation is typically used for custom or unsupported message types.
/// For example, the model may accept a JSON serialized message.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;
Expand Down Expand Up @@ -57,5 +58,13 @@ public RealtimeConversationItem(IList<AIContent> contents, string? id = null, Ch
/// Gets or sets the raw representation of the conversation item.
/// This can be used to hold the original data structure received from or sent to the provider.
/// </summary>
/// <remarks>
/// During serialization the converter attempts to serialize the runtime value using the active
/// <see cref="System.Text.Json.JsonSerializerOptions"/>. If the value cannot be serialized,
/// an empty JSON object is written as a fallback. During deserialization, the value is
/// always materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;
Expand Down Expand Up @@ -31,5 +32,7 @@ public class RealtimeServerMessage
/// The raw representation is typically used for custom or unsupported message types.
/// For example, the model may accept a JSON serialized server message.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ public SpeechToTextResponse(string? content)
/// <remarks>
/// If a <see cref="SpeechToTextResponse"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets any additional properties associated with the speech to text response.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ public SpeechToTextResponseUpdate(string? content)
/// <remarks>
/// If a <see cref="SpeechToTextResponseUpdate"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets additional properties for the update.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ public TextToSpeechResponse(IList<AIContent> contents)
/// <remarks>
/// If a <see cref="TextToSpeechResponse"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets any additional properties associated with the text to speech response.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ public TextToSpeechResponseUpdate(IList<AIContent> contents)
/// <remarks>
/// If a <see cref="TextToSpeechResponseUpdate"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// for enabling a consumer to access the underlying object model if needed. When serialized to JSON,
/// the value is written using the active <see cref="System.Text.Json.JsonSerializerOptions"/> when possible.
/// When deserialized from JSON, the value is materialized as a <see cref="System.Text.Json.JsonElement"/>.
/// </remarks>
[JsonIgnore]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(RawRepresentationJsonConverter))]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets additional properties for the update.</summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Provides best-effort JSON serialization for <c>RawRepresentation</c> properties.
/// </summary>
/// <remarks>
/// <para>
/// When writing JSON, the converter attempts to serialize the runtime value using the active
/// <see cref="JsonSerializerOptions"/>. If serialization fails (for example, due to circular references
/// or missing type metadata), it writes an empty JSON object (<c>{}</c>) as a fallback.
/// </para>
/// <para>
/// When reading JSON, it always materializes the payload as a <see cref="JsonElement"/>.
/// </para>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class RawRepresentationJsonConverter : JsonConverter<object?>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the exception of this eating serialization exceptions, this mostly coincides with how object serialization works out of the box. So maybe this could be simplified by delegating to the default object converter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that in the case of the built-in converter the actual representation can be controlled by the JsonUnknownTypeHandling enum.

{
/// <inheritdoc />
public override bool HandleNull => false;

/// <inheritdoc />
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.Null)
{
return null;
}

using JsonDocument document = JsonDocument.ParseValue(ref reader);
return document.RootElement.Clone();
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
_ = Throw.IfNull(writer);

if (value is null)
{
writer.WriteNullValue();
return;
}

if (value is JsonElement jsonElement)
{
jsonElement.WriteTo(writer);
return;
}

if (value is JsonDocument jsonDocument)
{
jsonDocument.RootElement.WriteTo(writer);
return;
}

_ = Throw.IfNull(options);

if (options.TryGetTypeInfo(value.GetType(), out JsonTypeInfo? typeInfo))
{
try
{
JsonSerializer.SerializeToElement(value, typeInfo).WriteTo(writer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably move the WriteTo call outside the try block since we want to still surface any exceptions related to the underlying writer.

Consider using SerializeToDocument with a using so that pooled buffers are used for the intermediate representation.

return;
}
catch (Exception e) when (e is JsonException or InvalidOperationException or NotSupportedException)
{
// Serialization failed; fall through to write empty object.
}
}

writer.WriteStartObject();
writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ namespace Microsoft.Extensions.AI;
/// The <see cref="DistributedCachingChatClient"/> employs JSON serialization as part of storing cached data. It is not guaranteed that
/// the object models used by <see cref="ChatMessage"/>, <see cref="ChatOptions"/>, <see cref="ChatResponse"/>, <see cref="ChatResponseUpdate"/>,
/// or any of the other objects in the chat client pipeline will roundtrip through JSON serialization with full fidelity. For example,
/// <see cref="ChatMessage.RawRepresentation"/> will be ignored, and <see cref="object"/> values in <see cref="ChatMessage.AdditionalProperties"/>
/// <see cref="ChatMessage.RawRepresentation"/> values will only roundtrip when their runtime values can be serialized, and they will deserialize as
/// <see cref="JsonElement"/> rather than as the original type. Likewise, <see cref="object"/> values in <see cref="ChatMessage.AdditionalProperties"/>
/// will deserialize as <see cref="JsonElement"/> rather than as the original type. In general, code using <see cref="DistributedCachingChatClient"/>
/// should only rely on accessing data that can be preserved well enough through JSON serialization and deserialization.
/// </para>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public static class DistributedCachingChatClientBuilderExtensions
/// The <see cref="DistributedCachingChatClient"/> employs JSON serialization as part of storing the cached data. It is not guaranteed that
/// the object models used by <see cref="ChatMessage"/>, <see cref="ChatOptions"/>, <see cref="ChatResponse"/>, <see cref="ChatResponseUpdate"/>,
/// or any of the other objects in the chat client pipeline will roundtrip through JSON serialization with full fidelity. For example,
/// <see cref="ChatMessage.RawRepresentation"/> will be ignored, and <see cref="object"/> values in <see cref="ChatMessage.AdditionalProperties"/>
/// <see cref="ChatMessage.RawRepresentation"/> values will only roundtrip when their runtime values can be serialized, and they will deserialize as
/// <see cref="JsonElement"/> rather than as the original type. Likewise, <see cref="object"/> values in <see cref="ChatMessage.AdditionalProperties"/>
/// will deserialize as <see cref="JsonElement"/> rather than as the original type. In general, code using <see cref="DistributedCachingChatClient"/>
/// should only rely on accessing data that can be preserved well enough through JSON serialization and deserialization.
/// </remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ public void ItCanBeSerializeAndDeserialized()
var chatMessage = new ChatMessage(ChatRole.User, contents: items)
{
AuthorName = "Fred",
RawRepresentation = new Dictionary<string, object?> { ["value"] = 42 },
AdditionalProperties = new() { ["message-metadata-key-1"] = "message-metadata-value-1" },
};
((TextContent)chatMessage.Contents[0]).Text = "content-1-override"; // Override the content of the first text content item that has the "content-1" content
Expand All @@ -286,6 +287,8 @@ public void ItCanBeSerializeAndDeserialized()
// Assert
Assert.Equal("Fred", deserializedMessage.AuthorName);
Assert.Equal("user", deserializedMessage.Role.Value);
JsonElement rawRepresentation = Assert.IsType<JsonElement>(deserializedMessage.RawRepresentation);
Assert.Equal(42, rawRepresentation.GetProperty("value").GetInt32());
Assert.NotNull(deserializedMessage.AdditionalProperties);
Assert.Single(deserializedMessage.AdditionalProperties);
Assert.Equal("message-metadata-value-1", deserializedMessage.AdditionalProperties["message-metadata-key-1"]?.ToString());
Expand Down
Loading
Loading