1use crate::error::TypeError;
2use crate::json_to_pyobject_value;
3use crate::util::{is_pydantic_model, pyobject_to_json};
4use crate::PyHelperFuncs;
5use chrono::DateTime;
6use chrono::Utc;
7use potato_head::create_uuid7;
8use potato_head::Prompt;
9use pyo3::prelude::*;
10use pyo3::types::{PyDict, PyFloat, PyInt, PyList, PyString};
11use pyo3::IntoPyObjectExt;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::fmt;
16use std::fmt::Display;
17use std::fmt::Formatter;
18
19#[pyclass]
20#[derive(Clone, Debug, Serialize)]
21pub enum EntityType {
22 Feature,
23 Metric,
24 LLM,
25}
26
27#[pyclass]
28#[derive(Clone, Serialize, Deserialize, Debug)]
29pub struct IntFeature {
30 pub name: String,
31 pub value: i64,
32}
33
34#[pymethods]
35impl IntFeature {
36 pub fn __str__(&self) -> String {
37 PyHelperFuncs::__str__(self)
38 }
39}
40
41impl IntFeature {
42 pub fn to_float(&self) -> f64 {
43 self.value as f64
44 }
45}
46
47#[pyclass]
48#[derive(Clone, Serialize, Deserialize, Debug)]
49pub struct FloatFeature {
50 pub name: String,
51 pub value: f64,
52}
53
54#[pymethods]
55impl FloatFeature {
56 pub fn __str__(&self) -> String {
57 PyHelperFuncs::__str__(self)
58 }
59}
60
61#[pyclass]
62#[derive(Clone, Serialize, Deserialize, Debug)]
63pub struct StringFeature {
64 pub name: String,
65 pub value: String,
66}
67
68#[pymethods]
69impl StringFeature {
70 pub fn __str__(&self) -> String {
71 PyHelperFuncs::__str__(self)
72 }
73}
74
75impl StringFeature {
76 pub fn to_float(&self, feature_map: &FeatureMap) -> Result<f64, TypeError> {
77 feature_map
78 .features
79 .get(&self.name)
80 .and_then(|feat_map| {
81 feat_map
82 .get(&self.value)
83 .or_else(|| feat_map.get("missing"))
84 })
85 .map(|&val| val as f64)
86 .ok_or(TypeError::MissingStringValueError)
87 }
88}
89
90#[pyclass]
91#[derive(Clone, Serialize, Deserialize, Debug)]
92pub enum Feature {
93 Int(IntFeature),
94 Float(FloatFeature),
95 String(StringFeature),
96}
97
98#[pymethods]
99impl Feature {
100 #[new]
101 pub fn new(name: &str, feature: Bound<'_, PyAny>) -> Result<Self, TypeError> {
113 if feature.is_instance_of::<PyFloat>() {
115 let value: f64 = feature.extract().unwrap();
116 Ok(Feature::Float(FloatFeature {
117 name: name.into(),
118 value,
119 }))
120 } else if feature.is_instance_of::<PyInt>() {
121 let value: i64 = feature.extract().unwrap();
122 Ok(Feature::Int(IntFeature {
123 name: name.into(),
124 value,
125 }))
126 } else if feature.is_instance_of::<PyString>() {
127 let value: String = feature.extract().unwrap();
128 Ok(Feature::String(StringFeature {
129 name: name.into(),
130 value,
131 }))
132 } else {
133 Err(TypeError::UnsupportedFeatureTypeError(
134 feature.get_type().name()?.to_string(),
135 ))
136 }
137 }
138
139 #[staticmethod]
140 pub fn int(name: String, value: i64) -> Self {
141 Feature::Int(IntFeature { name, value })
142 }
143
144 #[staticmethod]
145 pub fn float(name: String, value: f64) -> Self {
146 Feature::Float(FloatFeature { name, value })
147 }
148
149 #[staticmethod]
150 pub fn string(name: String, value: String) -> Self {
151 Feature::String(StringFeature { name, value })
152 }
153
154 #[staticmethod]
155 pub fn categorical(name: String, value: String) -> Self {
156 Feature::String(StringFeature { name, value })
157 }
158
159 pub fn __str__(&self) -> String {
160 PyHelperFuncs::__str__(self)
161 }
162}
163
164impl Feature {
165 pub fn to_float(&self, feature_map: &FeatureMap) -> Result<f64, TypeError> {
166 match self {
167 Feature::Int(feature) => Ok(feature.to_float()),
168 Feature::Float(feature) => Ok(feature.value),
169 Feature::String(feature) => feature.to_float(feature_map),
170 }
171 }
172
173 pub fn name(&self) -> &str {
174 match self {
175 Feature::Int(feature) => &feature.name,
176 Feature::Float(feature) => &feature.name,
177 Feature::String(feature) => &feature.name,
178 }
179 }
180
181 pub fn to_usize(&self, feature_map: &FeatureMap) -> Result<usize, TypeError> {
182 match self {
183 Feature::Int(f) => Ok(f.value as usize),
184 Feature::Float(f) => Ok(f.value as usize),
185 Feature::String(f) => Ok(f.to_float(feature_map)? as usize),
186 }
187 }
188}
189
190impl Display for Feature {
191 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
192 match self {
193 Feature::Int(feature) => write!(f, "{}", feature.value),
194 Feature::Float(feature) => write!(f, "{}", feature.value),
195 Feature::String(feature) => write!(f, "{}", feature.value),
196 }
197 }
198}
199
200#[pyclass]
201#[derive(Clone, Debug, Serialize)]
202pub struct Features {
203 #[pyo3(get)]
204 pub features: Vec<Feature>,
205
206 #[pyo3(get)]
207 pub entity_type: EntityType,
208}
209
210#[pymethods]
211impl Features {
212 #[new]
213 pub fn new(features: Bound<'_, PyAny>) -> Result<Self, TypeError> {
226 let features = if features.is_instance_of::<PyList>() {
227 features
228 .downcast::<PyList>()
229 .unwrap()
230 .iter()
231 .map(|item| item.extract::<Feature>().unwrap())
232 .collect()
233 } else if features.is_instance_of::<PyDict>() {
234 features
235 .downcast::<PyDict>()
236 .unwrap()
237 .iter()
238 .map(|(key, value)| {
239 Feature::new(&key.extract::<String>().unwrap(), value.clone()).unwrap()
240 })
241 .collect()
242 } else {
243 Err(TypeError::UnsupportedFeaturesTypeError(
244 features.get_type().name()?.to_string(),
245 ))?
246 };
247 Ok(Features {
248 features,
249 entity_type: EntityType::Feature,
250 })
251 }
252
253 pub fn __str__(&self) -> String {
254 PyHelperFuncs::__str__(self)
255 }
256}
257
258impl Features {
259 pub fn iter(&self) -> std::slice::Iter<'_, Feature> {
260 self.features.iter()
261 }
262}
263
264#[pyclass]
265#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
266pub struct FeatureMap {
267 #[pyo3(get)]
268 pub features: HashMap<String, HashMap<String, usize>>,
269}
270
271#[pymethods]
272impl FeatureMap {
273 pub fn __str__(&self) -> String {
274 PyHelperFuncs::__str__(self)
276 }
277}
278
279#[pyclass]
280#[derive(Clone, Serialize, Debug)]
281pub struct Metric {
282 pub name: String,
283 pub value: f64,
284}
285
286#[pymethods]
287impl Metric {
288 #[new]
289 pub fn new(name: String, value: Bound<'_, PyAny>) -> Self {
290 let value = if value.is_instance_of::<PyFloat>() {
291 value.extract::<f64>().unwrap()
292 } else if value.is_instance_of::<PyInt>() {
293 value.extract::<i64>().unwrap() as f64
294 } else {
295 panic!(
296 "Unsupported metric type: {}",
297 value.get_type().name().unwrap()
298 );
299 };
300 let lowercase_name = name.to_lowercase();
301 Metric {
302 name: lowercase_name,
303 value,
304 }
305 }
306
307 pub fn __str__(&self) -> String {
308 PyHelperFuncs::__str__(self)
309 }
310}
311
312impl Metric {
313 pub fn new_rs(name: String, value: f64) -> Self {
314 Metric { name, value }
315 }
316}
317
318#[pyclass]
319#[derive(Clone, Serialize, Debug)]
320pub struct Metrics {
321 #[pyo3(get)]
322 pub metrics: Vec<Metric>,
323
324 #[pyo3(get)]
325 pub entity_type: EntityType,
326}
327
328#[pymethods]
329impl Metrics {
330 #[new]
331 pub fn new(metrics: Bound<'_, PyAny>) -> Result<Self, TypeError> {
332 let metrics = if metrics.is_instance_of::<PyList>() {
333 metrics
334 .downcast::<PyList>()
335 .unwrap()
336 .iter()
337 .map(|item| item.extract::<Metric>().unwrap())
338 .collect()
339 } else if metrics.is_instance_of::<PyDict>() {
340 metrics
341 .downcast::<PyDict>()
342 .unwrap()
343 .iter()
344 .map(|(key, value)| Metric::new(key.extract().unwrap(), value))
345 .collect()
346 } else {
347 Err(TypeError::UnsupportedMetricsTypeError(
348 metrics.get_type().name()?.to_string(),
349 ))?
350 };
351 Ok(Metrics {
352 metrics,
353 entity_type: EntityType::Metric,
354 })
355 }
356 pub fn __str__(&self) -> String {
357 PyHelperFuncs::__str__(self)
358 }
359}
360
361impl Metrics {
362 pub fn iter(&self) -> std::slice::Iter<'_, Metric> {
363 self.metrics.iter()
364 }
365}
366
367#[pyclass]
368#[derive(Clone, Serialize, Debug)]
369pub struct LLMRecord {
370 pub uid: String,
371
372 pub space: String,
373
374 pub name: String,
375
376 pub version: String,
377
378 pub created_at: DateTime<Utc>,
379
380 pub context: Value,
381
382 pub score: Value,
383
384 pub prompt: Option<Value>,
385
386 #[pyo3(get)]
387 pub entity_type: EntityType,
388}
389
390#[pymethods]
391impl LLMRecord {
392 #[new]
393 #[pyo3(signature = (
394 context,
395 prompt=None,
396 ))]
397
398 pub fn new(
401 py: Python<'_>,
402 context: Bound<'_, PyAny>,
403 prompt: Option<Bound<'_, PyAny>>,
404 ) -> Result<Self, TypeError> {
405 let context_val = if context.is_instance_of::<PyDict>() {
407 pyobject_to_json(&context)?
408 } else if is_pydantic_model(py, &context)? {
409 let model = context.call_method0("model_dump")?;
411
412 pyobject_to_json(&model)?
414 } else {
415 Err(TypeError::MustBeDictOrBaseModel)?
416 };
417
418 let prompt: Option<Value> = match prompt {
419 Some(p) => {
420 if p.is_instance_of::<Prompt>() {
421 let prompt = p.extract::<Prompt>()?;
422 Some(serde_json::to_value(prompt)?)
423 } else {
424 Some(pyobject_to_json(&p)?)
425 }
426 }
427 None => None,
428 };
429
430 Ok(LLMRecord {
431 uid: create_uuid7(),
432 created_at: Utc::now(),
433 space: String::new(),
434 name: String::new(),
435 version: String::new(),
436 context: context_val,
437 score: Value::Null,
438 prompt,
439 entity_type: EntityType::LLM,
440 })
441 }
442
443 #[getter]
444 pub fn context<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
445 Ok(json_to_pyobject_value(py, &self.context)?
446 .into_bound_py_any(py)?
447 .clone())
448 }
449}
450
451impl LLMRecord {
452 pub fn new_rs(context: Option<Value>, prompt: Option<Value>) -> Self {
453 LLMRecord {
454 context: context.unwrap_or(Value::Object(serde_json::Map::new())),
455 prompt,
456 entity_type: EntityType::LLM,
457 uid: create_uuid7(),
458 created_at: Utc::now(),
459 space: String::new(),
460 name: String::new(),
461 version: String::new(),
462 score: Value::Null,
463 }
464 }
465
466 pub fn __str__(&self) -> String {
467 PyHelperFuncs::__str__(self)
468 }
469}
470
471#[derive(Debug)]
472pub enum QueueItem {
473 Features(Features),
474 Metrics(Metrics),
475 LLM(Box<LLMRecord>),
476}
477
478impl QueueItem {
479 pub fn from_py_entity(entity: &Bound<'_, PyAny>) -> Result<Self, TypeError> {
481 let entity_type = entity.getattr("entity_type")?.extract::<EntityType>()?;
482
483 match entity_type {
484 EntityType::Feature => {
485 let features = entity.extract::<Features>()?;
486 Ok(QueueItem::Features(features))
487 }
488 EntityType::Metric => {
489 let metrics = entity.extract::<Metrics>()?;
490 Ok(QueueItem::Metrics(metrics))
491 }
492 EntityType::LLM => {
493 let llm = entity.extract::<LLMRecord>()?;
495 Ok(QueueItem::LLM(Box::new(llm)))
496 }
497 }
498 }
499}
500
501pub trait QueueExt: Send + Sync {
502 fn metrics(&self) -> &Vec<Metric>;
503 fn features(&self) -> &Vec<Feature>;
504 fn llm_records(&self) -> Vec<&LLMRecord>;
505}
506
507impl QueueExt for Features {
508 fn metrics(&self) -> &Vec<Metric> {
509 static EMPTY: Vec<Metric> = Vec::new();
512 &EMPTY
513 }
514
515 fn features(&self) -> &Vec<Feature> {
516 &self.features
517 }
518
519 fn llm_records(&self) -> Vec<&LLMRecord> {
520 vec![]
523 }
524}
525
526impl QueueExt for Metrics {
527 fn metrics(&self) -> &Vec<Metric> {
528 &self.metrics
529 }
530
531 fn features(&self) -> &Vec<Feature> {
532 static EMPTY: Vec<Feature> = Vec::new();
535 &EMPTY
536 }
537
538 fn llm_records(&self) -> Vec<&LLMRecord> {
539 vec![]
542 }
543}
544
545impl QueueExt for LLMRecord {
546 fn metrics(&self) -> &Vec<Metric> {
547 static EMPTY: Vec<Metric> = Vec::new();
550 &EMPTY
551 }
552
553 fn features(&self) -> &Vec<Feature> {
554 static EMPTY: Vec<Feature> = Vec::new();
557 &EMPTY
558 }
559
560 fn llm_records(&self) -> Vec<&LLMRecord> {
561 vec![self]
562 }
563}