Skip to main content

scouter_types/genai/
alert.rs

1use crate::error::TypeError;
2use crate::AlertCondition;
3use crate::{
4    dispatch::AlertDispatchType, AlertDispatchConfig, AlertThreshold, CommonCrons,
5    DispatchAlertDescription, OpsGenieDispatchConfig, SlackDispatchConfig, ValidateAlertConfig,
6};
7use core::fmt::Debug;
8use pyo3::prelude::*;
9use pyo3::types::PyString;
10use serde::{Deserialize, Serialize};
11
12#[pyclass]
13#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
14pub struct GenAIAlertConfig {
15    pub dispatch_config: AlertDispatchConfig,
16
17    #[pyo3(get, set)]
18    pub schedule: String,
19
20    #[pyo3(get, set)]
21    pub alert_condition: Option<AlertCondition>,
22}
23
24impl ValidateAlertConfig for GenAIAlertConfig {}
25
26#[pymethods]
27impl GenAIAlertConfig {
28    #[new]
29    #[pyo3(signature = (schedule=None, dispatch_config=None, alert_condition=None))]
30    pub fn new(
31        schedule: Option<&Bound<'_, PyAny>>,
32        dispatch_config: Option<&Bound<'_, PyAny>>,
33        alert_condition: Option<AlertCondition>,
34    ) -> Result<Self, TypeError> {
35        let alert_dispatch_config = match dispatch_config {
36            None => AlertDispatchConfig::default(),
37            Some(config) => {
38                if config.is_instance_of::<SlackDispatchConfig>() {
39                    AlertDispatchConfig::Slack(config.extract::<SlackDispatchConfig>()?)
40                } else if config.is_instance_of::<OpsGenieDispatchConfig>() {
41                    AlertDispatchConfig::OpsGenie(config.extract::<OpsGenieDispatchConfig>()?)
42                } else {
43                    AlertDispatchConfig::default()
44                }
45            }
46        };
47
48        let schedule = match schedule {
49            Some(schedule) => {
50                if schedule.is_instance_of::<PyString>() {
51                    schedule.to_string()
52                } else if schedule.is_instance_of::<CommonCrons>() {
53                    schedule.extract::<CommonCrons>().unwrap().cron()
54                } else {
55                    return Err(TypeError::InvalidScheduleError)?;
56                }
57            }
58            None => CommonCrons::EveryDay.cron(),
59        };
60
61        let schedule = Self::resolve_schedule(&schedule);
62
63        Ok(Self {
64            schedule,
65            dispatch_config: alert_dispatch_config,
66            alert_condition,
67        })
68    }
69
70    #[getter]
71    pub fn dispatch_type(&self) -> AlertDispatchType {
72        self.dispatch_config.dispatch_type()
73    }
74
75    #[getter]
76    pub fn dispatch_config<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
77        self.dispatch_config.config(py)
78    }
79}
80
81impl Default for GenAIAlertConfig {
82    fn default() -> GenAIAlertConfig {
83        Self {
84            dispatch_config: AlertDispatchConfig::default(),
85            schedule: CommonCrons::EveryDay.cron(),
86            alert_condition: None,
87        }
88    }
89}
90
91pub struct PromptComparisonMetricAlert {
92    pub metric_name: String,
93    pub training_metric_value: f64,
94    pub observed_metric_value: f64,
95    pub alert_threshold_value: Option<f64>,
96    pub alert_threshold: AlertThreshold,
97}
98
99impl PromptComparisonMetricAlert {
100    fn alert_description_header(&self) -> String {
101        let below_threshold = |boundary: Option<f64>| match boundary {
102            Some(b) => format!(
103                "The observed {} metric value has dropped below the threshold (initial value - {})",
104                self.metric_name, b
105            ),
106            None => format!(
107                "The {} metric value has dropped below the initial value",
108                self.metric_name
109            ),
110        };
111
112        let above_threshold = |boundary: Option<f64>| match boundary {
113            Some(b) => format!(
114                "The {} metric value has increased beyond the threshold (initial value + {})",
115                self.metric_name, b
116            ),
117            None => format!(
118                "The {} metric value has increased beyond the initial value",
119                self.metric_name
120            ),
121        };
122
123        let outside_threshold = |boundary: Option<f64>| match boundary {
124            Some(b) => format!(
125                "The {} metric value has fallen outside the threshold (initial value ± {})",
126                self.metric_name, b,
127            ),
128            None => format!(
129                "The metric value has fallen outside the initial value for {}",
130                self.metric_name
131            ),
132        };
133
134        match self.alert_threshold {
135            AlertThreshold::Below => below_threshold(self.alert_threshold_value),
136            AlertThreshold::Above => above_threshold(self.alert_threshold_value),
137            AlertThreshold::Outside => outside_threshold(self.alert_threshold_value),
138        }
139    }
140}
141
142impl DispatchAlertDescription for PromptComparisonMetricAlert {
143    // TODO make pretty per dispatch type
144    fn create_alert_description(&self, _dispatch_type: AlertDispatchType) -> String {
145        let mut alert_description = String::new();
146        let header = format!("{}\n", self.alert_description_header());
147        alert_description.push_str(&header);
148
149        let current_metric = format!("Current Metric Value: {}\n", self.observed_metric_value);
150        let historical_metric = format!("Initial Metric Value: {}\n", self.training_metric_value);
151
152        alert_description.push_str(&historical_metric);
153        alert_description.push_str(&current_metric);
154
155        alert_description
156    }
157}
158
159#[cfg(test)]
160#[cfg(feature = "mock")]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_alert_config() {
166        //test console alert config
167        let dispatch_config = AlertDispatchConfig::OpsGenie(OpsGenieDispatchConfig {
168            team: "test-team".to_string(),
169            priority: "P5".to_string(),
170        });
171        let schedule = "0 0 * * * *".to_string();
172        let alert_condition = AlertCondition::new(5.0, AlertThreshold::Above, None);
173        let alert_config = GenAIAlertConfig {
174            dispatch_config,
175            schedule,
176            alert_condition: Some(alert_condition.clone()),
177        };
178        assert_eq!(alert_config.dispatch_type(), AlertDispatchType::OpsGenie);
179    }
180}