1use crate::error::UtilError;
2
3use colored_json::{Color, ColorMode, ColoredFormatter, PrettyFormatter, Styler};
4use pyo3::prelude::*;
5
6use pyo3::types::{PyAny, PyDict, PyList};
7use pyo3::IntoPyObjectExt;
8use serde::Serialize;
9use serde_json::Value;
10use serde_json::Value::{Null, Object};
11use std::ops::RangeInclusive;
12use std::path::Path;
13use uuid::Uuid;
14pub fn create_uuid7() -> String {
15 Uuid::now_v7().to_string()
16}
17use pythonize::{depythonize, pythonize};
18use tracing::warn;
19pub struct PyHelperFuncs {}
20
21impl PyHelperFuncs {
22 pub fn to_bound_py_object<'py, T>(
29 py: Python<'py>,
30 object: &T,
31 ) -> Result<Bound<'py, PyAny>, UtilError>
32 where
33 T: IntoPyObject<'py> + Clone,
34 {
35 Ok(object.clone().into_bound_py_any(py)?)
36 }
37 pub fn __str__<T: Serialize>(object: T) -> String {
38 match ColoredFormatter::with_styler(
39 PrettyFormatter::default(),
40 Styler {
41 key: Color::Rgb(75, 57, 120).foreground(),
42 string_value: Color::Rgb(4, 205, 155).foreground(),
43 float_value: Color::Rgb(4, 205, 155).foreground(),
44 integer_value: Color::Rgb(4, 205, 155).foreground(),
45 bool_value: Color::Rgb(4, 205, 155).foreground(),
46 nil_value: Color::Rgb(4, 205, 155).foreground(),
47 ..Default::default()
48 },
49 )
50 .to_colored_json(&object, ColorMode::On)
51 {
52 Ok(json) => json,
53 Err(e) => format!("Failed to serialize to json: {e}"),
54 }
55 }
57
58 pub fn __json__<T: Serialize>(object: T) -> String {
59 match serde_json::to_string_pretty(&object) {
60 Ok(json) => json,
61 Err(e) => format!("Failed to serialize to json: {e}"),
62 }
63 }
64
65 pub fn save_to_json<T>(model: T, path: &Path) -> Result<(), UtilError>
81 where
82 T: Serialize,
83 {
84 let json =
86 serde_json::to_string_pretty(&model).map_err(|_| UtilError::SerializationError)?;
87
88 let path = path.with_extension("json");
90
91 if !path.exists() {
92 let parent_path = path.parent().ok_or(UtilError::GetParentPathError)?;
94 if !parent_path.as_os_str().is_empty() {
95 std::fs::create_dir_all(parent_path)
96 .map_err(|_| UtilError::CreateDirectoryError)?;
97 }
98 }
99
100 std::fs::write(path, json).map_err(|_| UtilError::WriteError)?;
101
102 Ok(())
103 }
104}
105
106pub fn vec_to_py_object<'py>(
107 py: Python<'py>,
108 vec: &Vec<Value>,
109) -> Result<Bound<'py, PyList>, UtilError> {
110 let py_list = PyList::empty(py);
111 for item in vec {
112 let py_item = pythonize(py, item)?;
113 py_list.append(py_item)?;
114 }
115 Ok(py_list)
116}
117
118pub fn version() -> String {
119 env!("CARGO_PKG_VERSION").to_string()
120}
121
122pub fn update_serde_value(value: &mut Value, key: &str, new_value: Value) -> Result<(), UtilError> {
123 if let Value::Object(map) = value {
124 map.insert(key.to_string(), new_value);
125 Ok(())
126 } else {
127 Err(UtilError::RootMustBeObjectError)
128 }
129}
130
131pub fn update_serde_map_with(
142 dest: &mut serde_json::Value,
143 src: &serde_json::Value,
144) -> Result<(), UtilError> {
145 match (dest, src) {
146 (&mut Object(ref mut map_dest), Object(ref map_src)) => {
147 for (key, value) in map_src {
149 *map_dest.entry(key.clone()).or_insert(Null) = value.clone();
152 }
153 Ok(())
154 }
155 (_, _) => Err(UtilError::RootMustBeObjectError),
156 }
157}
158
159pub fn extract_string_value(py_value: &Bound<'_, PyAny>) -> Result<String, UtilError> {
161 if let Ok(string_val) = py_value.extract::<String>() {
163 return Ok(string_val);
164 }
165 if let Ok(bool_val) = py_value.extract::<bool>() {
167 return Ok(bool_val.to_string());
168 }
169
170 if let Ok(int_val) = py_value.extract::<i64>() {
172 return Ok(int_val.to_string());
173 }
174
175 if let Ok(float_val) = py_value.extract::<f64>() {
177 return Ok(float_val.to_string());
178 }
179
180 let json_value = depythonize(py_value)?;
182
183 match json_value {
184 Value::String(s) => Ok(s),
185 Value::Number(n) => Ok(n.to_string()),
186 Value::Bool(b) => Ok(b.to_string()),
187 Value::Null => Ok("null".to_string()),
188 _ => {
189 let json_string = serde_json::to_string(&json_value)?;
191 Ok(json_string)
192 }
193 }
194}
195
196#[pyclass(from_py_object)]
197#[derive(Debug, Serialize, Clone)]
198pub struct TokenLogProbs {
199 #[pyo3(get)]
200 pub token: String,
201
202 #[pyo3(get)]
203 pub logprob: f64,
204}
205
206#[pyclass(from_py_object)]
207#[derive(Debug, Serialize, Clone)]
208pub struct ResponseLogProbs {
209 #[pyo3(get)]
210 pub tokens: Vec<TokenLogProbs>,
211}
212
213#[pymethods]
214impl ResponseLogProbs {
215 pub fn __str__(&self) -> String {
216 PyHelperFuncs::__str__(self)
217 }
218}
219
220pub fn calculate_weighted_score(log_probs: &[TokenLogProbs]) -> Result<Option<f64>, UtilError> {
222 let score_range = RangeInclusive::new(1, 5);
223 let mut score_probs = Vec::new();
224 let mut weighted_sum = 0.0;
225 let mut total_prob = 0.0;
226
227 for log_prob in log_probs {
228 let token = log_prob.token.parse::<u64>().ok();
229
230 if let Some(token_val) = token {
231 if score_range.contains(&token_val) {
232 let prob = log_prob.logprob.exp();
233 score_probs.push((token_val, prob));
234 }
235 }
236 }
237
238 for (score, logprob) in score_probs {
239 weighted_sum += score as f64 * logprob;
240 total_prob += logprob;
241 }
242
243 if total_prob > 0.0 {
244 Ok(Some(weighted_sum / total_prob))
245 } else {
246 Ok(None)
247 }
248}
249
250pub fn convert_text_to_structured_output<'py>(
263 py: Python<'py>,
264 text: String,
265 output_model: &Bound<'py, PyAny>,
266) -> Result<Bound<'py, PyAny>, UtilError> {
267 let output = output_model.call_method1("model_validate_json", (&text,));
268 match output {
269 Ok(obj) => {
270 Ok(obj)
272 }
273 Err(err) => {
274 warn!(
277 "Failed to validate model: {}, Attempting fallback to JSON parsing",
278 err
279 );
280 let val = serde_json::from_str::<serde_json::Value>(&text)?;
281 Ok(pythonize(py, &val)?)
282 }
283 }
284}
285
286pub fn is_pydantic_basemodel(py: Python, obj: &Bound<'_, PyAny>) -> Result<bool, UtilError> {
287 let pydantic = match py.import("pydantic") {
288 Ok(module) => module,
289 Err(_) => return Ok(false),
291 };
292
293 let basemodel = pydantic.getattr("BaseModel")?;
294
295 let is_basemodel = obj
297 .is_instance(&basemodel)
298 .map_err(|e| UtilError::FailedToCheckPydanticModel(e.to_string()))?;
299
300 Ok(is_basemodel)
301}
302
303fn process_dict_with_nested_models(
304 py: Python<'_>,
305 dict: &Bound<'_, PyAny>,
306) -> Result<Value, UtilError> {
307 let py_dict = dict.cast::<PyDict>()?;
308 let mut result = serde_json::Map::new();
309
310 for (key, value) in py_dict.iter() {
311 let key_str: String = key.extract()?;
312 let processed_value = depythonize_object_to_value(py, &value)?;
313 result.insert(key_str, processed_value);
314 }
315
316 Ok(Value::Object(result))
317}
318
319pub fn depythonize_object_to_value<'py>(
320 py: Python<'py>,
321 value: &Bound<'py, PyAny>,
322) -> Result<Value, UtilError> {
323 let py_value = if is_pydantic_basemodel(py, value)? {
324 let model = value.call_method0("model_dump")?;
325 depythonize(&model)?
326 } else if value.is_instance_of::<PyDict>() {
327 process_dict_with_nested_models(py, value)?
328 } else {
329 depythonize(value)?
330 };
331 Ok(py_value)
332}
333
334pub fn construct_structured_response<'py>(
345 py: Python<'py>,
346 text: String,
347 output_model: Option<&Bound<'py, PyAny>>,
348) -> Result<Bound<'py, PyAny>, UtilError> {
349 match output_model {
350 Some(model) => convert_text_to_structured_output(py, text, model),
351 None => {
352 let val = Value::String(text);
354 Ok(pythonize(py, &val)?)
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 #[test]
363 fn test_calculate_weighted_score() {
364 let log_probs = vec![
365 TokenLogProbs {
366 token: "1".into(),
367 logprob: 0.9,
368 },
369 TokenLogProbs {
370 token: "2".into(),
371 logprob: 0.8,
372 },
373 TokenLogProbs {
374 token: "3".into(),
375 logprob: 0.7,
376 },
377 ];
378
379 let result = calculate_weighted_score(&log_probs);
380 assert!(result.is_ok());
381
382 let val = result.unwrap().unwrap();
383 assert_eq!(val.round(), 2.0);
385 }
386 #[test]
387 fn test_calculate_weighted_score_empty() {
388 let log_probs: Vec<TokenLogProbs> = vec![];
389 let result = calculate_weighted_score(&log_probs);
390 assert!(result.is_ok());
391 assert_eq!(result.unwrap(), None);
392 }
393}