feature: Explainable Alerts
This commit is contained in:
@@ -87,18 +87,32 @@ public class TrendDetector
|
||||
}
|
||||
}
|
||||
|
||||
var trendContext = TrendContextBuilder.BuildContext(observationCode, history, value);
|
||||
var details = TrendCalculator.DescribeTrend(observationCode, rate.Value, value);
|
||||
|
||||
var contributor = new List<ScoreContributor>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Parameter = TrendCalculator.DisplayName(observationCode),
|
||||
Points = 0,
|
||||
RawValue = value.ToString(),
|
||||
NormalRange = null
|
||||
}
|
||||
};
|
||||
var created = await TryCreateAlertAsync(
|
||||
encounterId, patientId, observationCode, details, rate.Value, ct);
|
||||
encounterId, patientId, observationCode, details, rate.Value,
|
||||
contributor, trendContext, ct);
|
||||
|
||||
return new TrendResult(
|
||||
created ? TrendOutcome.RapidDeterioration : TrendOutcome.AlertAlreadyOpen,
|
||||
observationCode, rate, created);
|
||||
observationCode, rate, created, trendContext);
|
||||
}
|
||||
|
||||
private async Task<bool> TryCreateAlertAsync(
|
||||
Guid encounterId, Guid patientId,
|
||||
string observationCode, string details, decimal ratePerMinute,
|
||||
List<ScoreContributor> contributors, TrendContext? trendContext,
|
||||
CancellationToken ct)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
@@ -110,21 +124,19 @@ public class TrendDetector
|
||||
var triggeredAt = DateTimeOffset.UtcNow;
|
||||
var fullDetails = $"{details} — velocity {ratePerMinute:F2}/min over {_options.WindowMinutes}min window.";
|
||||
|
||||
var affected = await db.Database.ExecuteSqlInterpolatedAsync($"""
|
||||
INSERT INTO clinical_alerts
|
||||
(id, encounter_id, patient_id, alert_type, severity, details, observation_code, status, triggered_at)
|
||||
SELECT {alertId}, {encounterId}, {patientId},
|
||||
'RAPID_DETERIORATION', 'WARNING', {fullDetails}, {observationCode}, 'OPEN', {triggeredAt}
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM clinical_alerts
|
||||
WHERE encounter_id = {encounterId}
|
||||
AND alert_type = 'RAPID_DETERIORATION'
|
||||
AND observation_code = {observationCode}
|
||||
AND status IN ('OPEN', 'ESCALATED')
|
||||
)
|
||||
""", ct);
|
||||
MedicationContext? medication = null;
|
||||
var correlation = scope.ServiceProvider.GetRequiredService<MedicationCorrelationHelper>();
|
||||
medication = await correlation.TryGetContextAsync(encounterId, observationCode, ct);
|
||||
|
||||
if (affected == 0)
|
||||
var explanation = AlertExplanationBuilder.Build(
|
||||
"Rapid deterioration", null, contributors, trendContext, medication);
|
||||
|
||||
var inserted = await ClinicalAlertFactory.TryInsertAsync(
|
||||
db, alertId, encounterId, patientId,
|
||||
AlertType.RapidDeterioration, AlertSeverity.Warning,
|
||||
fullDetails, explanation, observationCode, triggeredAt, ct);
|
||||
|
||||
if (!inserted)
|
||||
{
|
||||
await tx.RollbackAsync(ct);
|
||||
return false;
|
||||
@@ -134,7 +146,7 @@ public class TrendDetector
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Topic = "alert.generated",
|
||||
Payload = JsonSerializer.Serialize(new
|
||||
Payload = ClinicalAlertFactory.SerializeOutboxPayload(new
|
||||
{
|
||||
alertId,
|
||||
encounterId,
|
||||
@@ -145,6 +157,7 @@ public class TrendDetector
|
||||
triggeredAt,
|
||||
observationCode,
|
||||
ratePerMinute,
|
||||
explanation,
|
||||
partitionKey = encounterId.ToString()
|
||||
}),
|
||||
PartitionKey = encounterId.ToString(),
|
||||
|
||||
Reference in New Issue
Block a user