feature: Explainable Alerts

This commit is contained in:
voltsrage
2026-06-25 00:25:31 +08:00
parent 279add1e45
commit 666d683d67
61 changed files with 9553 additions and 125 deletions
@@ -0,0 +1,41 @@
public static class SofaContributorBuilder
{
private static readonly (string Label, string NormalRange)[] OrganSystems =
[
("Respiratory", "0"),
("Coagulation", "0"),
("Liver", "0"),
("Cardiovascular", "0"),
("CNS", "0"),
("Renal", "0"),
];
private static readonly string[] OrganKeys =
[
"RESPIRATORY", "COAGULATION", "LIVER", "CARDIOVASCULAR", "CNS", "RENAL"
];
public static List<ScoreContributor> Build(
SofaResult result,
IReadOnlyDictionary<string, string?>? rawValues = null)
{
var scores = new[]
{
result.Respiratory, result.Coagulation, result.Liver,
result.Cardiovascular, result.Cns, result.Renal
};
var contributors = new List<ScoreContributor>(scores.Length);
for (int i = 0; i < scores.Length; i++)
{
contributors.Add(new ScoreContributor
{
Parameter = OrganSystems[i].Label,
Points = scores[i],
RawValue = rawValues?.GetValueOrDefault(OrganKeys[i]),
NormalRange = OrganSystems[i].NormalRange
});
}
return contributors;
}
}
+31 -17
View File
@@ -147,6 +147,23 @@ public class SofaDetector
var result = SofaCalculator.ComputeTotal(
respiratory, coagulation, liver, cardiovascular, cns, renal);
var rawValues = new Dictionary<string, string?>
{
["RESPIRATORY"] = pao2 is not null && fio2 is not null
? $"PaO2/FiO2={pao2}/{fio2}%"
: usedSpO2Fallback && spo2 is not null && fio2 is not null
? $"SpO2/FiO2={spo2}/{fio2}%"
: null,
["COAGULATION"] = GetValue("PLATELET_K_UL")?.ToString(),
["LIVER"] = GetValue("BILIRUBIN_MG_DL")?.ToString(),
["CARDIOVASCULAR"] = vasopressor is not null
? $"{vasopressor.DrugName} {vasopressor.DoseUgKgMin} µg/kg/min"
: map?.ToString(),
["CNS"] = gcsTotal?.ToString(),
["RENAL"] = creatinine?.ToString() ?? urineMlDay?.ToString()
};
var contributors = SofaContributorBuilder.Build(result, rawValues);
var stalenessFlags = JsonSerializer.Serialize(new SofaStalenessFlags(
staleComponents.Distinct().ToList(),
missingComponents.Distinct().ToList(),
@@ -179,13 +196,13 @@ public class SofaDetector
{
alertCreated = await TryCreateAlertAsync(
encounterId, patientId, AlertType.SofaSepsis, AlertSeverity.Critical,
result, isBaseline ? null : delta, stalenessFlags, ct);
result, isBaseline ? null : delta, stalenessFlags, contributors, ct);
}
else if (delta == 1)
{
alertCreated = await TryCreateAlertAsync(
encounterId, patientId, AlertType.SofaWarning, AlertSeverity.Warning,
result, delta, stalenessFlags, ct);
result, delta, stalenessFlags, contributors, ct);
}
_metrics.SofaScoresTotal
@@ -197,7 +214,7 @@ public class SofaDetector
result.Total, encounterId, isBaseline, delta);
return new SofaScoringResult(
SofaOutcome.ScoreComputed, result, isBaseline, delta, alertCreated);
SofaOutcome.ScoreComputed, result, isBaseline, delta, alertCreated, contributors);
}
private async Task<int?> LoadGcsTotalAsync(Guid encounterId)
@@ -268,6 +285,7 @@ public class SofaDetector
Guid encounterId, Guid patientId,
AlertType alertType, AlertSeverity severity,
SofaResult result, int? delta, string stalenessFlags,
List<ScoreContributor> contributors,
CancellationToken ct)
{
if (alertType == AlertType.SofaWarning)
@@ -292,20 +310,15 @@ public class SofaDetector
$"Liver={result.Liver}, CV={result.Cardiovascular}, CNS={result.Cns}, Renal={result.Renal}. " +
$"Staleness: {stalenessFlags}";
var affected = await db.Database.ExecuteSqlInterpolatedAsync($"""
INSERT INTO clinical_alerts
(id, encounter_id, patient_id, alert_type, severity, details, status, triggered_at)
SELECT {alertId}, {encounterId}, {patientId},
{alertType.ToDbString()}, {severity.ToDbString()}, {details}, 'OPEN', {triggeredAt}
WHERE NOT EXISTS (
SELECT 1 FROM clinical_alerts
WHERE encounter_id = {encounterId}
AND alert_type = {alertType.ToDbString()}
AND status IN ('OPEN', 'ESCALATED')
)
""", ct);
var explanation = AlertExplanationBuilder.Build(
"SOFA", result.Total, contributors);
if (affected == 0)
var inserted = await ClinicalAlertFactory.TryInsertAsync(
db, alertId, encounterId, patientId,
alertType, severity, details, explanation,
observationCode: null, triggeredAt, ct);
if (!inserted)
{
await tx.RollbackAsync(ct);
return false;
@@ -315,7 +328,7 @@ public class SofaDetector
{
Id = Guid.NewGuid(),
Topic = "alert.generated",
Payload = JsonSerializer.Serialize(new
Payload = ClinicalAlertFactory.SerializeOutboxPayload(new
{
alertId,
encounterId,
@@ -326,6 +339,7 @@ public class SofaDetector
triggeredAt,
sofaTotal = result.Total,
sofaDelta = delta,
explanation,
partitionKey = encounterId.ToString()
}),
PartitionKey = encounterId.ToString(),