1use crate::error::TypeError;
2use crate::json_to_pyobject_value;
3use crate::util::{is_pydantic_model, pyobject_to_json};
4use crate::ProfileFuncs;
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 ProfileFuncs::__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 ProfileFuncs::__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 ProfileFuncs::__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 ProfileFuncs::__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)| Feature::new(key.extract().unwrap(), value.clone()).unwrap())
239 .collect()
240 } else {
241 Err(TypeError::UnsupportedFeaturesTypeError(
242 features.get_type().name()?.to_string(),
243 ))?
244 };
245 Ok(Features {
246 features,
247 entity_type: EntityType::Feature,
248 })
249 }
250
251 pub fn __str__(&self) -> String {
252 ProfileFuncs::__str__(self)
253 }
254}
255
256impl Features {
257 pub fn iter(&self) -> std::slice::Iter<'_, Feature> {
258 self.features.iter()
259 }
260}
261
262#[pyclass]
263#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
264pub struct FeatureMap {
265 #[pyo3(get)]
266 pub features: HashMap<String, HashMap<String, usize>>,
267}
268
269#[pymethods]
270impl FeatureMap {
271 pub fn __str__(&self) -> String {
272 ProfileFuncs::__str__(self)
274 }
275}
276
277#[pyclass]
278#[derive(Clone, Serialize, Debug)]
279pub struct Metric {
280 pub name: String,
281 pub value: f64,
282}
283
284#[pymethods]
285impl Metric {
286 #[new]
287 pub fn new(name: String, value: Bound<'_, PyAny>) -> Self {
288 let value = if value.is_instance_of::<PyFloat>() {
289 value.extract::<f64>().unwrap()
290 } else if value.is_instance_of::<PyInt>() {
291 value.extract::<i64>().unwrap() as f64
292 } else {
293 panic!(
294 "Unsupported metric type: {}",
295 value.get_type().name().unwrap()
296 );
297 };
298 let lowercase_name = name.to_lowercase();
299 Metric {
300 name: lowercase_name,
301 value,
302 }
303 }
304
305 pub fn __str__(&self) -> String {
306 ProfileFuncs::__str__(self)
307 }
308}
309
310impl Metric {
311 pub fn new_rs(name: String, value: f64) -> Self {
312 Metric { name, value }
313 }
314}
315
316#[pyclass]
317#[derive(Clone, Serialize, Debug)]
318pub struct Metrics {
319 #[pyo3(get)]
320 pub metrics: Vec<Metric>,
321
322 #[pyo3(get)]
323 pub entity_type: EntityType,
324}
325
326#[pymethods]
327impl Metrics {
328 #[new]
329 pub fn new(metrics: Bound<'_, PyAny>) -> Result<Self, TypeError> {
330 let metrics = if metrics.is_instance_of::<PyList>() {
331 metrics
332 .downcast::<PyList>()
333 .unwrap()
334 .iter()
335 .map(|item| item.extract::<Metric>().unwrap())
336 .collect()
337 } else if metrics.is_instance_of::<PyDict>() {
338 metrics
339 .downcast::<PyDict>()
340 .unwrap()
341 .iter()
342 .map(|(key, value)| Metric::new(key.extract().unwrap(), value))
343 .collect()
344 } else {
345 Err(TypeError::UnsupportedMetricsTypeError(
346 metrics.get_type().name()?.to_string(),
347 ))?
348 };
349 Ok(Metrics {
350 metrics,
351 entity_type: EntityType::Metric,
352 })
353 }
354 pub fn __str__(&self) -> String {
355 ProfileFuncs::__str__(self)
356 }
357}
358
359impl Metrics {
360 pub fn iter(&self) -> std::slice::Iter<'_, Metric> {
361 self.metrics.iter()
362 }
363}
364
365#[pyclass]
366#[derive(Clone, Serialize, Debug)]
367pub struct LLMRecord {
368 pub uid: String,
369
370 pub space: String,
371
372 pub name: String,
373
374 pub version: String,
375
376 pub created_at: DateTime<Utc>,
377
378 pub context: Value,
379
380 pub score: Value,
381
382 pub prompt: Option<Value>,
383
384 #[pyo3(get)]
385 pub entity_type: EntityType,
386}
387
388#[pymethods]
389impl LLMRecord {
390 #[new]
391 #[pyo3(signature = (
392 context,
393 prompt=None,
394 ))]
395
396 pub fn new(
399 py: Python<'_>,
400 context: Bound<'_, PyAny>,
401 prompt: Option<Bound<'_, PyAny>>,
402 ) -> Result<Self, TypeError> {
403 let context_val = if context.is_instance_of::<PyDict>() {
405 pyobject_to_json(&context)?
406 } else if is_pydantic_model(py, &context)? {
407 let model = context.call_method0("model_dump")?;
409
410 pyobject_to_json(&model)?
412 } else {
413 Err(TypeError::MustBeDictOrBaseModel)?
414 };
415
416 let prompt: Option<Value> = match prompt {
417 Some(p) => {
418 if p.is_instance_of::<Prompt>() {
419 let prompt = p.extract::<Prompt>()?;
420 Some(serde_json::to_value(prompt)?)
421 } else {
422 Some(pyobject_to_json(&p)?)
423 }
424 }
425 None => None,
426 };
427
428 Ok(LLMRecord {
429 uid: create_uuid7(),
430 created_at: Utc::now(),
431 space: String::new(),
432 name: String::new(),
433 version: String::new(),
434 context: context_val,
435 score: Value::Null,
436 prompt,
437 entity_type: EntityType::LLM,
438 })
439 }
440
441 #[getter]
442 pub fn context<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, TypeError> {
443 Ok(json_to_pyobject_value(py, &self.context)?
444 .into_bound_py_any(py)?
445 .clone())
446 }
447}
448
449impl LLMRecord {
450 pub fn new_rs(context: Option<Value>, prompt: Option<Value>) -> Self {
451 LLMRecord {
452 context: context.unwrap_or(Value::Object(serde_json::Map::new())),
453 prompt,
454 entity_type: EntityType::LLM,
455 uid: create_uuid7(),
456 created_at: Utc::now(),
457 space: String::new(),
458 name: String::new(),
459 version: String::new(),
460 score: Value::Null,
461 }
462 }
463
464 pub fn __str__(&self) -> String {
465 ProfileFuncs::__str__(self)
466 }
467}
468
469#[derive(Debug)]
470pub enum QueueItem {
471 Features(Features),
472 Metrics(Metrics),
473 LLM(Box<LLMRecord>),
474}
475
476impl QueueItem {
477 pub fn from_py_entity(entity: &Bound<'_, PyAny>) -> Result<Self, TypeError> {
479 let entity_type = entity.getattr("entity_type")?.extract::<EntityType>()?;
480
481 match entity_type {
482 EntityType::Feature => {
483 let features = entity.extract::<Features>()?;
484 Ok(QueueItem::Features(features))
485 }
486 EntityType::Metric => {
487 let metrics = entity.extract::<Metrics>()?;
488 Ok(QueueItem::Metrics(metrics))
489 }
490 EntityType::LLM => {
491 let llm = entity.extract::<LLMRecord>()?;
493 Ok(QueueItem::LLM(Box::new(llm)))
494 }
495 }
496 }
497}
498
499pub trait QueueExt: Send + Sync {
500 fn metrics(&self) -> &Vec<Metric>;
501 fn features(&self) -> &Vec<Feature>;
502 fn llm_records(&self) -> Vec<&LLMRecord>;
503}
504
505impl QueueExt for Features {
506 fn metrics(&self) -> &Vec<Metric> {
507 static EMPTY: Vec<Metric> = Vec::new();
510 &EMPTY
511 }
512
513 fn features(&self) -> &Vec<Feature> {
514 &self.features
515 }
516
517 fn llm_records(&self) -> Vec<&LLMRecord> {
518 vec![]
521 }
522}
523
524impl QueueExt for Metrics {
525 fn metrics(&self) -> &Vec<Metric> {
526 &self.metrics
527 }
528
529 fn features(&self) -> &Vec<Feature> {
530 static EMPTY: Vec<Feature> = Vec::new();
533 &EMPTY
534 }
535
536 fn llm_records(&self) -> Vec<&LLMRecord> {
537 vec![]
540 }
541}
542
543impl QueueExt for LLMRecord {
544 fn metrics(&self) -> &Vec<Metric> {
545 static EMPTY: Vec<Metric> = Vec::new();
548 &EMPTY
549 }
550
551 fn features(&self) -> &Vec<Feature> {
552 static EMPTY: Vec<Feature> = Vec::new();
555 &EMPTY
556 }
557
558 fn llm_records(&self) -> Vec<&LLMRecord> {
559 vec![self]
560 }
561}