Files
step3/api/QuestionsApi/Models/QuestionResponse.cs
T

176 lines
8.2 KiB
C#

namespace QuestionsApi.Models;
public class QuestionResponse
{
public string Id { get; set; } = "";
public string ExternalId { get; set; } = "";
public string Stem { get; set; } = "";
public string LeadIn { get; set; } = "";
public List<QuestionChoice> Choices { get; set; } = [];
public string CorrectChoiceId { get; set; } = "";
public string Explanation { get; set; } = "";
public string KeyTakeaway { get; set; } = "";
public string UsmlePearl { get; set; } = "";
public List<QuestionReviewFollowup> ReviewFollowups { get; set; } = [];
public Dictionary<string, string> ChoiceExplanations { get; set; } = [];
public Dictionary<string, WrongAnswerFollowup> WrongAnswerFollowups { get; set; } = [];
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
public string ChoiceSelectionRateBasis { get; set; } = "presented";
public string Discipline { get; set; } = "";
public string System { get; set; } = "";
public string PerformanceCategory { get; set; } = "";
public int Difficulty { get; set; }
public int QuestionElo { get; set; } = 1000;
public List<QuestionLab> Labs { get; set; } = [];
public List<string> Media { get; set; } = [];
public List<string> Tags { get; set; } = [];
public List<QuestionReference> References { get; set; } = [];
public static QuestionResponse FromModel(
Question question,
Dictionary<string, int>? liveChoiceSelectionPercentages = null,
int questionElo = 1000,
bool includeChoicePool = true)
{
var choices = includeChoicePool ? question.Choices : SelectLegacyChoices(question);
var choiceIds = choices.Select(choice => choice.Id).ToHashSet(StringComparer.Ordinal);
var percentages = BuildChoiceSelectionPercentages(question, liveChoiceSelectionPercentages);
return new QuestionResponse
{
Id = question.Id ?? "",
ExternalId = question.ExternalId,
Stem = question.Stem,
LeadIn = question.LeadIn,
Choices = choices,
CorrectChoiceId = question.CorrectChoiceId,
Explanation = question.Explanation,
KeyTakeaway = question.KeyTakeaway,
UsmlePearl = question.UsmlePearl,
ReviewFollowups = question.ReviewFollowups,
ChoiceExplanations = question.ChoiceExplanations
.Where(entry => choiceIds.Contains(entry.Key))
.ToDictionary(),
WrongAnswerFollowups = question.WrongAnswerFollowups
.Where(entry => choiceIds.Contains(entry.Key))
.ToDictionary(),
ChoiceSelectionPercentages = percentages
.Where(entry => choiceIds.Contains(entry.Key))
.ToDictionary(),
Discipline = question.Discipline,
System = question.System,
PerformanceCategory = RatingCategory.Normalize(question.PerformanceCategory, question.System, question.Discipline),
Difficulty = question.Difficulty,
QuestionElo = questionElo,
Labs = question.Labs,
Media = question.Media,
Tags = question.Tags,
References = question.References
};
}
public static QuestionResponse FromModelWithChoices(
Question question,
IReadOnlyCollection<string> presentedChoiceIds,
int questionElo = 1000)
{
var response = FromModel(question, questionElo: questionElo, includeChoicePool: true);
var ids = presentedChoiceIds.ToHashSet(StringComparer.Ordinal);
response.Choices = presentedChoiceIds
.Select(id => question.Choices.FirstOrDefault(choice => choice.Id == id))
.Where(choice => choice is not null)
.Cast<QuestionChoice>()
.ToList();
response.ChoiceExplanations = response.ChoiceExplanations.Where(entry => ids.Contains(entry.Key)).ToDictionary();
response.WrongAnswerFollowups = response.WrongAnswerFollowups.Where(entry => ids.Contains(entry.Key)).ToDictionary();
response.ChoiceSelectionPercentages = response.ChoiceSelectionPercentages.Where(entry => ids.Contains(entry.Key)).ToDictionary();
return response;
}
private static List<QuestionChoice> SelectLegacyChoices(Question question)
{
if (question.Choices.Count <= 5)
return question.Choices;
var correct = question.Choices.First(choice => choice.Id == question.CorrectChoiceId);
var distractors = question.Choices
.Where(choice => choice.Id != question.CorrectChoiceId)
.OrderBy(_ => Random.Shared.Next())
.Take(4)
.Append(correct)
.OrderBy(_ => Random.Shared.Next())
.ToList();
return distractors;
}
public static Dictionary<string, int> BuildChoiceSelectionPercentages(Question question, Dictionary<string, int>? liveChoiceSelectionPercentages)
{
if (liveChoiceSelectionPercentages is not { Count: > 0 })
return question.ChoiceSelectionPercentages;
return question.Choices.ToDictionary(
choice => choice.Id,
choice => liveChoiceSelectionPercentages.GetValueOrDefault(choice.Id, 0));
}
}
public class FirstQuestionAttemptResponse
{
public string QuestionExternalId { get; set; } = "";
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
public int QuestionElo { get; set; } = 1000;
public RatingChangeResponse? Rating { get; set; }
}
public class SkipQuestionResponse
{
public string QuestionExternalId { get; set; } = "";
public string CorrectChoiceId { get; set; } = "";
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
public int QuestionElo { get; set; } = 1000;
public RatingChangeResponse? Rating { get; set; }
}
public static class RatingCategory
{
public const string Uncategorized = "Uncategorized";
public static readonly string[] Categories =
[
"Renal/Urinary & Male/Female Systems & Pregnancy",
"Behavioral Health & Social Sciences: Communication, Ethics, Patient Safety",
"Immune/Blood & Lymphoreticular/Endocrine/Multisystem",
"Biostatistics & Epidemiology/Population Health",
"Musculoskeletal/Skin & Subcutaneous Tissue",
"Cardiovascular System",
"Nervous System & Special Senses",
"Respiratory System",
"Gastrointestinal System",
Uncategorized
];
public static string Normalize(string? explicitCategory, string? system, string? discipline)
{
foreach (var value in new[] { explicitCategory, system, discipline })
{
if (string.IsNullOrWhiteSpace(value))
continue;
var normalized = Categories.FirstOrDefault(c => string.Equals(c, value.Trim(), StringComparison.OrdinalIgnoreCase));
if (normalized is not null)
return normalized;
var lower = value.ToLowerInvariant();
if (lower.Contains("cardio")) return "Cardiovascular System";
if (lower.Contains("resp")) return "Respiratory System";
if (lower.Contains("gastro") || lower.Contains("hepato")) return "Gastrointestinal System";
if (lower.Contains("neuro") || lower.Contains("special sense")) return "Nervous System & Special Senses";
if (lower.Contains("renal") || lower.Contains("urinary") || lower.Contains("pregnan") || lower.Contains("reproductive")) return "Renal/Urinary & Male/Female Systems & Pregnancy";
if (lower.Contains("behavior") || lower.Contains("ethic") || lower.Contains("communication") || lower.Contains("safety")) return "Behavioral Health & Social Sciences: Communication, Ethics, Patient Safety";
if (lower.Contains("immune") || lower.Contains("blood") || lower.Contains("endocrine") || lower.Contains("multisystem")) return "Immune/Blood & Lymphoreticular/Endocrine/Multisystem";
if (lower.Contains("biostat") || lower.Contains("epidemi") || lower.Contains("population")) return "Biostatistics & Epidemiology/Population Health";
if (lower.Contains("musculo") || lower.Contains("skin") || lower.Contains("derm")) return "Musculoskeletal/Skin & Subcutaneous Tissue";
}
return Uncategorized;
}
}