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,9 @@
public static class GcsContributorBuilder
{
public static List<ScoreContributor> Build(int eye, int verbal, int motor) => new()
{
new() { Parameter = "Eye", Points = eye, RawValue = eye.ToString(), NormalRange = "4" },
new() { Parameter = "Verbal", Points = verbal, RawValue = verbal.ToString(), NormalRange = "5" },
new() { Parameter = "Motor", Points = motor, RawValue = motor.ToString(), NormalRange = "6" },
};
}
+17 -17
View File
@@ -75,18 +75,20 @@ public class GcsDetector
encounterId, patientId, (int)eye, (int)verbal, (int)motor,
total, classification, calculatedAt, ct);
var contributors = GcsContributorBuilder.Build((int)eye, (int)verbal, (int)motor);
var alertCreated = false;
if (total <= 8)
{
alertCreated = await TryCreateAlertAsync(
encounterId, patientId, AlertType.GcsCritical, AlertSeverity.Critical,
eye, verbal, motor, total, classification, ct);
eye, verbal, motor, total, classification, contributors, ct);
}
else if (total <= 12)
{
alertCreated = await TryCreateAlertAsync(
encounterId, patientId, AlertType.GcsWarning, AlertSeverity.Warning,
eye, verbal, motor, total, classification, ct);
eye, verbal, motor, total, classification, contributors, ct);
}
await PublishScoredEventAsync(
@@ -105,7 +107,8 @@ public class GcsDetector
"GCS score {Total} ({Classification}) for encounter {Id} — E={Eye} V={Verbal} M={Motor}",
total, classification, encounterId, eye, verbal, motor);
return new GcsResult(GcsOutcome.ScoreComputed, total, classification, alertCreated, 3);
return new GcsResult(
GcsOutcome.ScoreComputed, total, classification, alertCreated, 3, contributors);
}
private async Task PersistScoreAsync(
@@ -140,6 +143,7 @@ public class GcsDetector
AlertType alertType, AlertSeverity severity,
decimal eye, decimal verbal, decimal motor,
int total, string classification,
List<ScoreContributor> contributors,
CancellationToken ct)
{
if (alertType == AlertType.GcsWarning)
@@ -164,20 +168,15 @@ public class GcsDetector
var details =
$"GCS total {total} ({classification}): E={eye}, V={verbal}, M={motor}.";
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(
"GCS", 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;
@@ -187,7 +186,7 @@ public class GcsDetector
{
Id = Guid.NewGuid(),
Topic = "alert.generated",
Payload = JsonSerializer.Serialize(new
Payload = ClinicalAlertFactory.SerializeOutboxPayload(new
{
alertId,
encounterId,
@@ -198,6 +197,7 @@ public class GcsDetector
triggeredAt,
gcsTotal = total,
gcsClassification = classification,
explanation,
partitionKey = encounterId.ToString()
}),
PartitionKey = encounterId.ToString(),