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