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
+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(),