| File: Model\GenAI\GenAIMessages.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Microsoft.OpenApi; namespace Aspire.Dashboard.Model.GenAI; /// <summary> /// Represents text, tool calls, data parts, and generic parts. /// </summary> [JsonConverter(typeof(MessagePartConverter))] public abstract class MessagePart { public const string TextType = "text"; public const string ToolCallType = "tool_call"; public const string ToolCallResponseType = "tool_call_response"; public const string BlobType = "blob"; public const string FileType = "file"; public const string UriType = "uri"; public const string ReasoningType = "reasoning"; public const string ServerToolCallType = "server_tool_call"; public const string ServerToolCallResponseType = "server_tool_call_response"; public string Type { get; set; } = default!; } /// <summary> /// Represents text content sent to or received from the model. /// </summary> public class TextPart : MessagePart { public TextPart() { Type = TextType; } public string? Content { get; set; } = default!; } /// <summary> /// Represents a tool call requested by the model. /// </summary> public class ToolCallRequestPart : MessagePart { public ToolCallRequestPart() { Type = ToolCallType; } public string? Id { get; set; } public string? Name { get; set; } = default!; public JsonNode? Arguments { get; set; } } /// <summary> /// Represents a tool call result sent to the model. /// </summary> public class ToolCallResponsePart : MessagePart { public ToolCallResponsePart() { Type = ToolCallResponseType; } public string? Id { get; set; } public JsonNode? Response { get; set; } = default!; } /// <summary> /// Represents blob binary data sent inline to the model. /// </summary> public class BlobPart : MessagePart { public BlobPart() { Type = BlobType; } public string? MimeType { get; set; } public string? Modality { get; set; } public string? Content { get; set; } } /// <summary> /// Represents an external referenced file sent to the model by file ID. /// </summary> public class FilePart : MessagePart { public FilePart() { Type = FileType; } public string? MimeType { get; set; } public string? Modality { get; set; } public string? FileId { get; set; } } /// <summary> /// Represents an external referenced file sent to the model by URI. /// </summary> public class UriPart : MessagePart { public UriPart() { Type = UriType; } public string? MimeType { get; set; } public string? Modality { get; set; } public string? Uri { get; set; } } /// <summary> /// Represents reasoning/thinking content received from the model. /// </summary> public class ReasoningPart : MessagePart { public ReasoningPart() { Type = ReasoningType; } public string? Content { get; set; } } /// <summary> /// Represents a server-side tool call invocation. /// </summary> public class ServerToolCallPart : MessagePart { public ServerToolCallPart() { Type = ServerToolCallType; } public string? Id { get; set; } public string? Name { get; set; } public JsonNode? ServerToolCall { get; set; } } /// <summary> /// Represents a server-side tool call response. /// </summary> public class ServerToolCallResponsePart : MessagePart { public ServerToolCallResponsePart() { Type = ServerToolCallResponseType; } public string? Id { get; set; } public JsonNode? ServerToolCallResponse { get; set; } } /// <summary> /// Represents an arbitrary message part with any type and properties. /// </summary> public class GenericPart : MessagePart { // Extensible dynamic properties [JsonExtensionData] public Dictionary<string, JsonElement>? AdditionalProperties { get; set; } } /// <summary> /// Represents a message part that failed to deserialize as its expected type. /// This is not part of the GenAI standard. It exists to handle unexpected errors when reading JSON, /// allowing the remaining data to still be displayed. /// </summary> public class UnexpectedErrorPart : GenericPart { [JsonIgnore] public Exception? Error { get; set; } } /// <summary> /// A chat message containing a role and parts. /// </summary> public class ChatMessage { public string Role { get; set; } = default!; public List<MessagePart> Parts { get; set; } = new(); // Only set on output message. public string? FinishReason { get; set; } } /// <summary> /// Represents a tool definition that can be used by the model. /// </summary> [DebuggerDisplay("Type = {Type}, Name = {Name}")] public class ToolDefinition { public string Type { get; set; } = "function"; public string? Name { get; set; } public string? Description { get; set; } public OpenApiSchema? Parameters { get; set; } } /// <summary> /// Handles polymorphic serialization and deserialization of <see cref="MessagePart"/> types. /// </summary> internal sealed class MessagePartConverter : JsonConverter<MessagePart> { public override MessagePart? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); string? type = null; try { if (!doc.RootElement.TryGetProperty("type", out var typeProp)) { throw new JsonException("Missing 'type' property."); } type = typeProp.GetString(); return type switch { MessagePart.TextType => doc.RootElement.Deserialize<TextPart>(options), MessagePart.ToolCallType => TryParseStringArguments(doc.RootElement.Deserialize<ToolCallRequestPart>(options)), MessagePart.ToolCallResponseType => doc.RootElement.Deserialize<ToolCallResponsePart>(options), MessagePart.BlobType => doc.RootElement.Deserialize<BlobPart>(options), MessagePart.FileType => doc.RootElement.Deserialize<FilePart>(options), MessagePart.UriType => doc.RootElement.Deserialize<UriPart>(options), MessagePart.ReasoningType => doc.RootElement.Deserialize<ReasoningPart>(options), MessagePart.ServerToolCallType => TryParseServerToolCallArguments(doc.RootElement.Deserialize<ServerToolCallPart>(options)), MessagePart.ServerToolCallResponseType => doc.RootElement.Deserialize<ServerToolCallResponsePart>(options), _ => doc.RootElement.Deserialize<GenericPart>(options), }; } catch (JsonException ex) { var wrappedEx = new InvalidOperationException($"Error reading message part '{type ?? "(missing)"}': {ex.Message}", ex); UnexpectedErrorPart? errorPart = null; try { errorPart = doc.RootElement.Deserialize<UnexpectedErrorPart>(options); } catch { // Ignore. We rethrow the original exception. } if (errorPart is null) { throw wrappedEx; } errorPart.Error = wrappedEx; return errorPart; } } public override void Write(Utf8JsonWriter writer, MessagePart value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, (object)value, value.GetType(), options); } /// <summary> /// If the tool call arguments are a string, try parsing them as JSON and replace with the parsed object. /// </summary> private static ToolCallRequestPart? TryParseStringArguments(ToolCallRequestPart? part) { if (part is { Arguments: not null }) { part.Arguments = GenAIMessageParsingHelper.TryParseStringJsonNode(part.Arguments); } return part; } private static ServerToolCallPart? TryParseServerToolCallArguments(ServerToolCallPart? part) { if (part is { ServerToolCall: not null }) { part.ServerToolCall = GenAIMessageParsingHelper.TryParseStringJsonNode(part.ServerToolCall); } return part; } } [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)] [JsonSerializable(typeof(MessagePart))] [JsonSerializable(typeof(TextPart))] [JsonSerializable(typeof(ToolCallRequestPart))] [JsonSerializable(typeof(ToolCallResponsePart))] [JsonSerializable(typeof(BlobPart))] [JsonSerializable(typeof(FilePart))] [JsonSerializable(typeof(UriPart))] [JsonSerializable(typeof(ReasoningPart))] [JsonSerializable(typeof(ServerToolCallPart))] [JsonSerializable(typeof(ServerToolCallResponsePart))] [JsonSerializable(typeof(GenericPart))] [JsonSerializable(typeof(UnexpectedErrorPart))] [JsonSerializable(typeof(ChatMessage))] [JsonSerializable(typeof(List<ChatMessage>))] public sealed partial class GenAIMessagesContext : JsonSerializerContext;