Skip to main content

scouter_types/
error.rs

1use pyo3::exceptions::PyRuntimeError;
2use pyo3::pyclass::PyClassGuardError;
3use pyo3::PyErr;
4use pythonize::PythonizeError;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum UtilError {
9    #[error("Failed to get parent path")]
10    GetParentPathError,
11
12    #[error("Failed to create directory")]
13    CreateDirectoryError,
14
15    #[error("Failed to read to create path")]
16    CreatePathError,
17
18    #[error(transparent)]
19    IoError(#[from] std::io::Error),
20
21    #[error(transparent)]
22    SerdeJsonError(#[from] serde_json::Error),
23}
24
25impl From<UtilError> for PyErr {
26    fn from(err: UtilError) -> PyErr {
27        let msg = err.to_string();
28        PyRuntimeError::new_err(msg)
29    }
30}
31
32#[derive(Error, Debug)]
33pub enum TypeError {
34    #[error("{0}")]
35    Error(String),
36
37    #[error("Start time must be before end time")]
38    StartTimeError,
39
40    #[error("Invalid schedule")]
41    InvalidScheduleError,
42
43    #[error("Invalid PSI threshold configuration")]
44    InvalidPsiThresholdError,
45
46    #[error("Invalid time interval for converting to start and end times")]
47    InvalidTimeIntervalError,
48
49    #[error("Invalid alert dispatch configuration")]
50    InvalidDispatchConfigError,
51
52    #[error("Invalid equal width binning method")]
53    InvalidEqualWidthBinningMethodError,
54
55    #[error("Missing space argument")]
56    MissingSpaceError,
57
58    #[error("Missing name argument")]
59    MissingNameError,
60
61    #[error("Missing version argument")]
62    MissingVersionError,
63
64    #[error("Missing uid argument")]
65    MissingUidError,
66
67    #[error("Missing alert_config argument")]
68    MissingAlertConfigError,
69
70    #[error("No metrics found")]
71    NoMetricsError,
72
73    #[error(transparent)]
74    SerdeJsonError(#[from] serde_json::Error),
75
76    #[error("Invalid number")]
77    InvalidNumberError,
78
79    #[error("Root must be an object")]
80    RootMustBeObject,
81
82    #[error("Unsupported type: {0}")]
83    UnsupportedType(String),
84
85    #[error("Failed to downcast Python object: {0}")]
86    DowncastError(String),
87
88    #[error("Invalid data type")]
89    InvalidDataType,
90
91    #[error("Missing value for string feature")]
92    MissingStringValueError,
93
94    #[error("{0}")]
95    PyError(String),
96
97    #[error(
98        "Unsupported feature type. Feature must be an integer, float or string. Received: {0}"
99    )]
100    UnsupportedFeatureTypeError(String),
101
102    #[error("Unsupported features type. Features must be a list of Feature instances or a dictionary of key value pairs. Received: {0}")]
103    UnsupportedFeaturesTypeError(String),
104
105    #[error("Unsupported metrics type. Metrics must be a list of Metric instances or a dictionary of key value pairs. Received: {0}")]
106    UnsupportedMetricsTypeError(String),
107
108    #[error("{0}")]
109    InvalidParameterError(String),
110
111    #[error("{0}")]
112    InvalidBinCountError(String),
113
114    #[error("{0}")]
115    InvalidValueError(String),
116
117    #[error("Empty Array Detected: {0}")]
118    EmptyArrayError(String),
119
120    #[error("Invalid binning strategy")]
121    InvalidBinningStrategyError,
122
123    #[error("Unsupported status. Status must be one of: All, Pending or Processed. Received: {0}")]
124    InvalidStatusError(String),
125
126    #[error("Failed to supply either input or response for the genai record")]
127    MissingInputOrResponse,
128
129    #[error("Invalid context type. Context must be a PyDict or a Pydantic BaseModel")]
130    MustBeDictOrBaseModel,
131
132    #[error("Failed to check if the context is a Pydantic BaseModel. Error: {0}")]
133    FailedToCheckPydanticModel(String),
134
135    #[error("Failed to import pydantic module. Error: {0}")]
136    FailedToImportPydantic(String),
137
138    #[error("Unsupported Python object type for conversion")]
139    UnsupportedPyObjectType,
140
141    #[error("Invalid dictionary key type. Dictionary keys must be strings, int, float or bool")]
142    InvalidDictKeyType,
143
144    #[error("Invalid compressions type")]
145    InvalidCompressionTypeError,
146
147    #[error("Invalid evaluation task type: {0}")]
148    InvalidEvalType(String),
149
150    #[error("Compression type not supported: {0}")]
151    CompressionTypeNotSupported(String),
152
153    #[error("Missing dependency: {0}")]
154    MissingDependency(String),
155
156    #[error("Expected a Python dict")]
157    ExpectedPyDict,
158
159    #[error("List contains an item that is neither AssertionTask nor LLMJudgeTask")]
160    InvalidAssertionTaskType,
161
162    #[error("{0}")]
163    FailedToCreateProfile(String),
164
165    #[error("Duplicate task IDs found in evaluation tasks")]
166    DuplicateTaskIds,
167
168    #[error("Key not found: {key}")]
169    KeyNotFound { key: String },
170
171    #[error("{0}")]
172    InvalidLength(String),
173
174    #[error(transparent)]
175    FromHexError(#[from] hex::FromHexError),
176
177    #[error(transparent)]
178    StdIOError(#[from] std::io::Error),
179
180    #[error(transparent)]
181    SerdeYamlError(#[from] serde_yaml::Error),
182
183    #[error("Array {index} out of bounds for length {length}")]
184    IndexOutOfBounds { index: isize, length: usize },
185
186    #[error("Expected an integer index or a slice")]
187    IndexOrSliceExpected,
188
189    #[error(transparent)]
190    PotatoTypeError(#[from] potato_head::TypeError),
191}
192
193impl From<pythonize::PythonizeError> for TypeError {
194    fn from(err: PythonizeError) -> Self {
195        TypeError::PyError(err.to_string())
196    }
197}
198
199impl<'a, 'py> From<pyo3::CastError<'a, 'py>> for TypeError {
200    fn from(err: pyo3::CastError<'a, 'py>) -> Self {
201        TypeError::DowncastError(err.to_string())
202    }
203}
204
205impl From<TypeError> for PyErr {
206    fn from(err: TypeError) -> PyErr {
207        let msg = err.to_string();
208        PyRuntimeError::new_err(msg)
209    }
210}
211
212impl From<PyErr> for TypeError {
213    fn from(err: PyErr) -> TypeError {
214        TypeError::PyError(err.to_string())
215    }
216}
217
218impl<'a, 'py> From<PyClassGuardError<'a, 'py>> for TypeError {
219    fn from(err: PyClassGuardError<'a, 'py>) -> Self {
220        TypeError::PyError(err.to_string())
221    }
222}
223
224#[derive(Error, Debug)]
225pub enum ContractError {
226    #[error(transparent)]
227    TypeError(#[from] TypeError),
228
229    #[error("{0}")]
230    PyError(String),
231}
232
233impl From<ContractError> for PyErr {
234    fn from(err: ContractError) -> PyErr {
235        let msg = err.to_string();
236        PyRuntimeError::new_err(msg)
237    }
238}
239
240impl From<PyErr> for ContractError {
241    fn from(err: PyErr) -> ContractError {
242        ContractError::PyError(err.to_string())
243    }
244}
245
246#[derive(Error, Debug)]
247pub enum RecordError {
248    #[error("Unable to extract record into any known ServerRecord variant")]
249    ExtractionError,
250
251    #[error("No server records found")]
252    EmptyServerRecordsError,
253
254    #[error(transparent)]
255    SerdeJsonError(#[from] serde_json::Error),
256
257    #[error("Unexpected record type")]
258    InvalidDriftTypeError,
259
260    #[error("{0}")]
261    PyError(String),
262
263    #[error("Failed to supply either input or response for the genai record")]
264    MissingInputOrResponse,
265
266    #[error(transparent)]
267    PotatoUtilError(#[from] potato_head::UtilError),
268
269    #[error(transparent)]
270    TypeError(#[from] TypeError),
271
272    #[error("Invalid context type. Context must be dictionary or Pydantic BaseModel")]
273    MustBeDictOrBaseModel,
274
275    #[error("Failed to downcast Python object: {0}")]
276    DowncastError(String),
277
278    #[error("{0}")]
279    SliceError(String),
280
281    #[error(transparent)]
282    FromHexError(#[from] hex::FromHexError),
283}
284
285impl<'a, 'py> From<pyo3::CastError<'a, 'py>> for RecordError {
286    fn from(err: pyo3::CastError) -> Self {
287        RecordError::DowncastError(err.to_string())
288    }
289}
290
291impl From<pythonize::PythonizeError> for RecordError {
292    fn from(err: PythonizeError) -> Self {
293        RecordError::PyError(err.to_string())
294    }
295}
296
297impl From<RecordError> for PyErr {
298    fn from(err: RecordError) -> PyErr {
299        let msg = err.to_string();
300        PyRuntimeError::new_err(msg)
301    }
302}
303
304impl From<PyErr> for RecordError {
305    fn from(err: PyErr) -> RecordError {
306        RecordError::PyError(err.to_string())
307    }
308}
309
310impl<'a, 'py> From<PyClassGuardError<'a, 'py>> for RecordError {
311    fn from(err: PyClassGuardError<'a, 'py>) -> Self {
312        RecordError::PyError(err.to_string())
313    }
314}
315
316#[derive(Error, Debug)]
317pub enum ProfileError {
318    #[error(transparent)]
319    SerdeJsonError(#[from] serde_json::Error),
320
321    #[error("Features and array are not the same length")]
322    FeatureArrayLengthError,
323
324    #[error("Unexpected record type")]
325    InvalidDriftTypeError,
326
327    #[error(transparent)]
328    UtilError(#[from] UtilError),
329
330    #[error(transparent)]
331    TypeError(#[from] TypeError),
332
333    #[error(transparent)]
334    IoError(#[from] std::io::Error),
335
336    #[error("Missing sample argument")]
337    MissingSampleError,
338
339    #[error("Missing sample size argument")]
340    MissingSampleSizeError,
341
342    #[error("Custom alert thresholds have not been set")]
343    CustomThresholdNotSetError,
344
345    #[error("Custom alert threshold not found")]
346    CustomAlertThresholdNotFound,
347
348    #[error("{0}")]
349    PyError(String),
350
351    #[error("Invalid binning strategy")]
352    InvalidBinningStrategyError,
353
354    #[error("Missing evaluation workflow")]
355    MissingWorkflowError,
356
357    #[error("Invalid argument for workflow. Argument must be a Workflow object")]
358    InvalidWorkflowType,
359
360    #[error(transparent)]
361    AgentError(#[from] potato_head::AgentError),
362
363    #[error(transparent)]
364    WorkflowError(#[from] potato_head::WorkflowError),
365
366    #[error("Invalid metric name found: {0}")]
367    InvalidMetricNameError(String),
368
369    #[error("No AssertionTasks or LLMJudgeTasks found in the workflow")]
370    EmptyTaskList,
371
372    #[error("LLM Metric requires at least one bound parameter")]
373    NeedAtLeastOneBoundParameterError(String),
374
375    #[error(
376        "Missing prompt in LLM Metric. If providing a list of metrics, prompt must be present"
377    )]
378    MissingPromptError(String),
379
380    #[error("No tasks found in the workflow when validating: {0}")]
381    NoTasksFoundError(String),
382
383    #[error("No metrics found for the output task: {0}")]
384    MetricNotFoundForOutputTask(String),
385
386    #[error("Metric not found in profile LLM metrics: {0}")]
387    MetricNotFound(String),
388
389    #[error(transparent)]
390    PotatoTypeError(#[from] potato_head::TypeError),
391
392    #[error("Invalid task type. Expected either AssertionTask or LLMJudgeTask: {0}")]
393    InvalidTaskType(String),
394
395    #[error("Detected circular dependency in evaluation tasks")]
396    CircularDependency,
397}
398
399impl From<ProfileError> for PyErr {
400    fn from(err: ProfileError) -> PyErr {
401        let msg = err.to_string();
402        PyRuntimeError::new_err(msg)
403    }
404}
405
406impl From<PyErr> for ProfileError {
407    fn from(err: PyErr) -> ProfileError {
408        ProfileError::PyError(err.to_string())
409    }
410}
411
412impl<'a, 'py> From<PyClassGuardError<'a, 'py>> for ProfileError {
413    fn from(err: PyClassGuardError<'a, 'py>) -> Self {
414        ProfileError::PyError(err.to_string())
415    }
416}
417
418impl<'a, 'py> From<pyo3::CastError<'a, 'py>> for ProfileError {
419    fn from(err: pyo3::CastError) -> Self {
420        ProfileError::PyError(err.to_string())
421    }
422}