| File: Model\GenAI\GenAIMessageParsingHelper.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.Text; using System.Text.Json; using System.Text.Json.Nodes; namespace Aspire.Dashboard.Model.GenAI; /// <summary> /// Provides truncation-tolerant JSON array deserialization for GenAI message parsing. /// </summary> internal static class GenAIMessageParsingHelper { internal delegate T? ReadElement<T>(ref Utf8JsonReader reader); internal static (List<T> items, bool truncated) DeserializeArrayIncrementally<T>(string json, ReadElement<T> readElement) { var bytes = Encoding.UTF8.GetBytes(json); var readerOptions = new JsonReaderOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; var reader = new Utf8JsonReader(bytes, readerOptions); return DeserializeArrayIncrementally(ref reader, readElement); } internal static (List<T> items, bool truncated) DeserializeArrayIncrementally<T>(ref Utf8JsonReader reader, ReadElement<T> readElement) { var items = new List<T>(); // Read start of array. Let exceptions propagate for truly invalid JSON. if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray) { throw new JsonException("Expected a JSON array."); } while (true) { bool readSuccess; try { readSuccess = reader.Read(); } catch (JsonException) { return (items, true); } if (!readSuccess) { return (items, true); } if (reader.TokenType == JsonTokenType.EndArray) { break; } try { var item = readElement(ref reader); if (item is not null) { items.Add(item); } } catch (JsonException) { return (items, true); } catch (InvalidOperationException) { return (items, true); } } return (items, false); } internal static MessagePart? ReadMessagePart(ref Utf8JsonReader reader) { return JsonSerializer.Deserialize(ref reader, GenAIMessagesContext.Default.MessagePart); } internal static (string role, List<MessagePart> parts, bool partsTruncated) ReadChatMessage(ref Utf8JsonReader reader) { if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException("Expected start of chat message object."); } string? role = null; List<MessagePart>? parts = null; var partsTruncated = false; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { break; } if (reader.TokenType != JsonTokenType.PropertyName) { throw new JsonException("Expected property name."); } var propertyName = reader.GetString(); switch (propertyName) { case "role": if (!reader.Read()) { throw new JsonException("Unexpected end of JSON while reading role value."); } role = reader.GetString(); break; case "parts": // DeserializeArrayIncrementally reads the StartArray token itself. (parts, partsTruncated) = DeserializeArrayIncrementally<MessagePart>(ref reader, ReadMessagePart); break; default: if (!reader.Read()) { throw new JsonException("Unexpected end of JSON while reading property value."); } if (!reader.TrySkip()) { throw new JsonException("Unexpected end of JSON while skipping property value."); } break; } } return (role ?? string.Empty, parts ?? [], partsTruncated); } /// <summary> /// If the node is a JSON string that parses to a JSON object or array, return the parsed node instead. /// </summary> internal static JsonNode? TryParseStringJsonNode(JsonNode? node) { if (node?.GetValueKind() == JsonValueKind.String && node.GetValue<string>() is { } json) { try { var parsed = JsonNode.Parse(json); if (parsed?.GetValueKind() is JsonValueKind.Object or JsonValueKind.Array) { return parsed; } } catch (JsonException) { // Not valid JSON. Keep the original string value. } } return node; } }