namespace AIStudio.Assistants.SWOTAnalysis; internal static class SwotAnalysisModerationResponseParser { /// /// Reads the answer of the moderator and checks that the result can be assembled from it. /// /// The answer of the model. /// The answer, or an empty one when reading failed. /// Why reading failed, written for the model; empty on success. /// Whether the answer could be read. public static bool TryParse(string rawResponse, out SwotModerationResult result, out string issue) { result = new(); if (!SwotModelAnswer.TryDeserialize(rawResponse, out var candidate, out issue)) return false; Normalize(candidate); issue = FindIssue(candidate); if (issue.Length > 0) return false; result = candidate; return true; } private static void Normalize(SwotModerationResult result) { result.MatrixHeading = result.MatrixHeading.Trim(); result.InternalLabel = result.InternalLabel.Trim(); result.ExternalLabel = result.ExternalLabel.Trim(); result.PositiveLabel = result.PositiveLabel.Trim(); result.NegativeLabel = result.NegativeLabel.Trim(); foreach (var category in Enum.GetValues().Select(result.CategoryOf)) { category.Label = category.Label.Trim(); category.EmptyMessage = category.EmptyMessage.Trim(); category.Findings.RemoveAll(SwotModelAnswer.IsNull); foreach (var finding in category.Findings) { finding.FactorId = finding.FactorId.Trim(); finding.Summary = finding.Summary.Trim(); finding.Explanation = finding.Explanation.Trim(); } } var notes = result.ModerationNotes; notes.Label = notes.Label.Trim(); notes.EmptyMessage = notes.EmptyMessage.Trim(); notes.Items.RemoveAll(SwotModelAnswer.IsNull); foreach (var note in notes.Items) { note.Change = note.Change.Trim(); note.Reason = note.Reason.Trim(); } result.OverallAssessment.Label = result.OverallAssessment.Label.Trim(); result.OverallAssessment.Text = result.OverallAssessment.Text.Trim(); var actions = result.PrioritizedActions; actions.Label = actions.Label.Trim(); actions.EmptyMessage = actions.EmptyMessage.Trim(); actions.Items.RemoveAll(SwotModelAnswer.IsNull); foreach (var action in actions.Items) { action.Action = action.Action.Trim(); action.Rationale = action.Rationale.Trim(); action.AddressedFactorIds = action.AddressedFactorIds .Where(factorId => !string.IsNullOrWhiteSpace(factorId)) .Select(factorId => factorId.Trim()) .ToList(); } } private static string FindIssue(SwotModerationResult result) { var fields = new Dictionary { ["matrix_heading"] = result.MatrixHeading, ["internal_label"] = result.InternalLabel, ["external_label"] = result.ExternalLabel, ["positive_label"] = result.PositiveLabel, ["negative_label"] = result.NegativeLabel, ["strengths.label"] = result.Strengths.Label, ["weaknesses.label"] = result.Weaknesses.Label, ["opportunities.label"] = result.Opportunities.Label, ["threats.label"] = result.Threats.Label, ["moderation_notes.label"] = result.ModerationNotes.Label, ["overall_assessment.label"] = result.OverallAssessment.Label, ["overall_assessment.text"] = result.OverallAssessment.Text, ["prioritized_actions.label"] = result.PrioritizedActions.Label, }; var emptyField = fields.FirstOrDefault(field => field.Value.Length == 0).Key; if (emptyField is not null) return $"The field {emptyField} is empty."; var factorIds = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var category in Enum.GetValues()) { var issue = FindIssue(category, result.CategoryOf(category), factorIds); if (issue.Length > 0) return issue; } var notes = result.ModerationNotes; var notesIssue = FindListIssue("moderation_notes", notes.Items.Count, notes.EmptyMessage); if (notesIssue.Length > 0) return notesIssue; for (var index = 0; index < notes.Items.Count; index++) { if (notes.Items[index].Change.Length == 0) return $"Note {index + 1} of moderation_notes has no change."; if (notes.Items[index].Reason.Length == 0) return $"Note {index + 1} of moderation_notes has no reason."; } var actions = result.PrioritizedActions; var actionsIssue = FindListIssue("prioritized_actions", actions.Items.Count, actions.EmptyMessage); if (actionsIssue.Length > 0) return actionsIssue; for (var index = 0; index < actions.Items.Count; index++) { var action = actions.Items[index]; if (action.Action.Length == 0) return $"Action {index + 1} has no action."; if (action.Rationale.Length == 0) return $"Action {index + 1} has no rationale."; if (action.AddressedFactorIds.Count == 0) return $"Action {index + 1} names no addressed_factor_ids."; var unknownFactorId = action.AddressedFactorIds.FirstOrDefault(factorId => !factorIds.Contains(factorId)); if (unknownFactorId is not null) return $"Action {index + 1} names the factor ID \"{unknownFactorId}\", which no final finding has. Use only the factor_id values of the final findings."; } return string.Empty; } /// /// Checks the findings of one category, and collects their factor IDs. /// /// The category. /// The category as the moderator returned it. /// The factor IDs found so far, to which the IDs of this category are added. /// The first issue, or an empty string. private static string FindIssue(SwotAnalysisCategory category, SwotModeratedCategory moderatedCategory, HashSet factorIds) { var name = category.JsonName; var listIssue = FindListIssue(name, moderatedCategory.Findings.Count, moderatedCategory.EmptyMessage); if (listIssue.Length > 0) return listIssue; var prefix = category.FactorIdPrefix; for (var index = 0; index < moderatedCategory.Findings.Count; index++) { var finding = moderatedCategory.Findings[index]; if (finding.Summary.Length == 0) return $"Finding {index + 1} of {name} has no summary."; if (finding.Explanation.Length == 0) return $"Finding {index + 1} of {name} has no explanation."; if (!finding.FactorId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return $"Finding {index + 1} of {name} has the factor_id \"{finding.FactorId}\", which does not start with \"{prefix}\"."; if (!factorIds.Add(finding.FactorId)) return $"The factor_id \"{finding.FactorId}\" is used more than once. Give every finding a factor_id of its own."; } return string.Empty; } /// /// Checks that a list either has items or explains in its empty message why it has none. /// private static string FindListIssue(string name, int itemCount, string emptyMessage) { if (itemCount == 0 && emptyMessage.Length == 0) return $"{name} has no items and no empty_message which explains why."; if (itemCount > 0 && emptyMessage.Length > 0) return $"{name} has items and an empty_message at once. Leave empty_message empty when you return items."; return string.Empty; } }