loopsmith_core/config/
alerts.rs1use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
20#[serde(rename_all = "snake_case")]
21pub enum Metric {
22 Iterations,
24 TokensUsed,
26 CostUsd,
28 WallClockSeconds,
30 FailedDispatches,
32 Retries,
34 StaleIterations,
36 ValidationPassRate,
38}
39
40impl Metric {
41 pub fn as_str(self) -> &'static str {
42 match self {
43 Metric::Iterations => "iterations",
44 Metric::TokensUsed => "tokens_used",
45 Metric::CostUsd => "cost_usd",
46 Metric::WallClockSeconds => "wall_clock_seconds",
47 Metric::FailedDispatches => "failed_dispatches",
48 Metric::Retries => "retries",
49 Metric::StaleIterations => "stale_iterations",
50 Metric::ValidationPassRate => "validation_pass_rate",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
57#[serde(deny_unknown_fields)]
58pub struct Alert {
59 pub id: String,
61 pub metric: Metric,
62 #[serde(default)]
64 pub above: Option<f64>,
65 #[serde(default)]
67 pub below: Option<f64>,
68 #[serde(default)]
71 pub message: Option<String>,
72}
73
74impl Alert {
75 pub fn fires_at(&self, value: f64) -> bool {
77 self.above.is_some_and(|t| value > t) || self.below.is_some_and(|t| value < t)
78 }
79
80 pub fn describe(&self, value: f64) -> String {
82 if let Some(m) = &self.message {
83 return format!("{} ({} = {})", m, self.metric.as_str(), trim(value));
84 }
85 let bound = match (self.above, self.below) {
86 (Some(t), _) if value > t => format!("above {}", trim(t)),
87 (_, Some(t)) if value < t => format!("below {}", trim(t)),
88 _ => "outside its bounds".to_string(),
89 };
90 format!("{} is {} ({bound})", self.metric.as_str(), trim(value))
91 }
92}
93
94fn trim(v: f64) -> String {
95 if v.fract() == 0.0 && v.abs() < 1e15 {
96 format!("{}", v as i64)
97 } else {
98 format!("{v:.4}")
99 .trim_end_matches('0')
100 .trim_end_matches('.')
101 .to_string()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn alert(above: Option<f64>, below: Option<f64>) -> Alert {
110 Alert {
111 id: "a".into(),
112 metric: Metric::CostUsd,
113 above,
114 below,
115 message: None,
116 }
117 }
118
119 #[test]
120 fn an_alert_fires_strictly_past_its_threshold() {
121 let a = alert(Some(2.0), None);
122 assert!(!a.fires_at(2.0), "at the threshold is not past it");
123 assert!(a.fires_at(2.01));
124 let b = alert(None, Some(0.5));
125 assert!(b.fires_at(0.4));
126 assert!(!b.fires_at(0.5));
127 }
128
129 #[test]
130 fn the_default_message_names_the_metric_and_the_bound() {
131 assert_eq!(
132 alert(Some(2.0), None).describe(3.5),
133 "cost_usd is 3.5 (above 2)"
134 );
135 }
136}