using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace AIStudio.Assistants.SWOTAnalysis; /// /// Reads the JSON object out of the answer of a model which works on the SWOT analysis. /// internal static class SwotModelAnswer { private static readonly JsonSerializerOptions JSON_OPTIONS = new() { PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, RespectNullableAnnotations = true, }; /// /// Reads the JSON object of a model answer into the given type, without checking its content. /// /// /// A null where the type expects a value fails the reading, and the issue names the field. A null /// inside a list, though, is read as such, since the annotations of list items do not count. /// /// The answer of the model. /// The object, or null when reading failed. /// Why reading failed, written for the model; empty on success. /// Whether the answer holds an object which could be read. public static bool TryDeserialize(string answer, [NotNullWhen(true)] out T? value, out string issue) where T : class { value = null; issue = string.Empty; var json = ModelAnswerJson.ExtractObject(answer); if (json.IsWhiteSpace()) { issue = "The answer contains no complete JSON object."; return false; } try { value = JsonSerializer.Deserialize(json, JSON_OPTIONS); } catch (JsonException e) { issue = $"The JSON object cannot be read: {e.Message}"; return false; } if (value is null) { issue = "The JSON object is empty."; return false; } return true; } /// /// Reads an answer, and asks the model once more when the answer cannot be read. /// /// /// The caller sends the repair request into the same thread, so the model sees its own answer next /// to what is wrong with it. Exactly one round, as in the Slide Assistant: a model which misses the /// form twice in a row seldom hits it the third time, and every round costs as much as the first. /// /// The first answer of the model. /// Reads an answer and names what is wrong with it. /// Gets the issue of the first answer, sends RepairRequestOf it, and returns the new answer; an empty one when the provider failed. /// The result of the first answer which could be read, or the issue of the last one. public static async Task> ReadWithOneRepairAsync(string answer, SwotAnswerReader read, Func> askAgain) where TResult : class { if (read(answer, out var result, out var issue)) return new(result, string.Empty); var repairedAnswer = await askAgain(issue); if (string.IsNullOrWhiteSpace(repairedAnswer)) return new(null, issue); return read(repairedAnswer, out result, out issue) ? new(result, string.Empty) : new(null, issue); } /// /// The request which asks a model for its answer once more, in the expected form. /// /// What is wrong with the answer, as the parser named it. public static string RepairRequestOf(string issue) => $""" Your answer cannot be used yet: {issue} Answer with the complete corrected JSON object and nothing else. """; /// /// Whether an item of a list read from a model answer is null. /// /// /// For removing the nulls a model wrote into a list: the annotations of list items do not count /// when reading, so a list may hold a null although its type says otherwise. /// /// The item of the list. /// Whether the item is null. public static bool IsNull(object? item) => item is null; }