use crate::error::TypeError;
use crate::EvalRecord;
use crate::PyHelperFuncs;
use opentelemetry::StringValue;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyFloat, PyInt, PyList, PyString};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use tracing::error;
#[pyclass]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EntityType {
Feature,
Metric,
GenAI,
}
impl From<EntityType> for opentelemetry::Value {
fn from(val: EntityType) -> Self {
match val {
EntityType::Feature => opentelemetry::Value::String(StringValue::from("Feature")),
EntityType::Metric => opentelemetry::Value::String(StringValue::from("Metric")),
EntityType::GenAI => opentelemetry::Value::String(StringValue::from("GenAI")),
}
}
}
impl Display for EntityType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
EntityType::Feature => write!(f, "Feature"),
EntityType::Metric => write!(f, "Metric"),
EntityType::GenAI => write!(f, "GenAI"),
}
}
}
#[pyclass]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IntFeature {
pub name: String,
pub value: i64,
}
#[pymethods]
impl IntFeature {
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
impl IntFeature {
pub fn to_float(&self) -> f64 {
self.value as f64
}
}
#[pyclass]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct FloatFeature {
pub name: String,
pub value: f64,
}
#[pymethods]
impl FloatFeature {
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
#[pyclass]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct StringFeature {
pub name: String,
pub value: String,
}
#[pymethods]
impl StringFeature {
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
impl StringFeature {
fn to_numeric<T>(&self, feature_map: &FeatureMap) -> Result<T, TypeError>
where
T: From<i32>,
{
feature_map
.features
.get(&self.name)
.and_then(|feat_map| {
feat_map
.get(&self.value)
.or_else(|| feat_map.get("missing"))
})
.copied()
.map(T::from)
.ok_or(TypeError::MissingStringValueError)
}
pub fn to_float(&self, feature_map: &FeatureMap) -> Result<f64, TypeError> {
self.to_numeric::<i32>(feature_map).map(|v| v as f64)
}
pub fn to_i32(&self, feature_map: &FeatureMap) -> Result<i32, TypeError> {
self.to_numeric::<i32>(feature_map)
}
}
#[pyclass(name = "QueueFeature")]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum Feature {
Int(IntFeature),
Float(FloatFeature),
String(StringFeature),
}
#[pymethods]
impl Feature {
#[new]
pub fn new(name: &str, feature: Bound<'_, PyAny>) -> Result<Self, TypeError> {
if feature.is_instance_of::<PyFloat>() {
let value: f64 = feature.extract().unwrap();
Ok(Feature::Float(FloatFeature {
name: name.into(),
value,
}))
} else if feature.is_instance_of::<PyInt>() {
let value: i64 = feature.extract().unwrap();
Ok(Feature::Int(IntFeature {
name: name.into(),
value,
}))
} else if feature.is_instance_of::<PyString>() {
let value: String = feature.extract().unwrap();
Ok(Feature::String(StringFeature {
name: name.into(),
value,
}))
} else {
Err(TypeError::UnsupportedFeatureTypeError(
feature.get_type().name()?.to_string(),
))
}
}
#[staticmethod]
pub fn int(name: String, value: i64) -> Self {
Feature::Int(IntFeature { name, value })
}
#[staticmethod]
pub fn float(name: String, value: f64) -> Self {
Feature::Float(FloatFeature { name, value })
}
#[staticmethod]
pub fn string(name: String, value: String) -> Self {
Feature::String(StringFeature { name, value })
}
#[staticmethod]
pub fn categorical(name: String, value: String) -> Self {
Feature::String(StringFeature { name, value })
}
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
impl Feature {
pub fn to_float(&self, feature_map: &FeatureMap) -> Result<f64, TypeError> {
match self {
Feature::Int(feature) => Ok(feature.to_float()),
Feature::Float(feature) => Ok(feature.value),
Feature::String(feature) => feature.to_float(feature_map),
}
}
pub fn name(&self) -> &str {
match self {
Feature::Int(feature) => &feature.name,
Feature::Float(feature) => &feature.name,
Feature::String(feature) => &feature.name,
}
}
pub fn to_usize(&self, feature_map: &FeatureMap) -> Result<usize, TypeError> {
match self {
Feature::Int(f) => Ok(f.value as usize),
Feature::Float(f) => Ok(f.value as usize),
Feature::String(f) => Ok(f.to_float(feature_map)? as usize),
}
}
pub fn to_i32(&self, feature_map: &FeatureMap) -> Result<i32, TypeError> {
match self {
Feature::Int(f) => Ok(f.value as i32),
Feature::Float(f) => Ok(f.value as i32),
Feature::String(f) => Ok(f.to_i32(feature_map)?),
}
}
}
impl Display for Feature {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Feature::Int(feature) => write!(f, "{}", feature.value),
Feature::Float(feature) => write!(f, "{}", feature.value),
Feature::String(feature) => write!(f, "{}", feature.value),
}
}
}
#[pyclass]
#[derive(Clone, Debug, Serialize)]
pub struct Features {
#[pyo3(get)]
pub features: Vec<Feature>,
#[pyo3(get)]
pub entity_type: EntityType,
}
#[pymethods]
impl Features {
#[new]
pub fn new(features: Bound<'_, PyAny>) -> Result<Self, TypeError> {
let features = if features.is_instance_of::<PyList>() {
let feature_list = features.cast::<PyList>()?;
let mut result = Vec::with_capacity(feature_list.len());
for item in feature_list.iter() {
result.push(item.extract::<Feature>()?);
}
result
} else if features.is_instance_of::<PyDict>() {
let dict = features.cast::<PyDict>()?;
let mut result = Vec::with_capacity(dict.len());
for (key, value) in dict.iter() {
let name = key.extract::<String>()?;
result.push(Feature::new(&name, value)?);
}
result
} else {
return Err(TypeError::UnsupportedFeaturesTypeError(
features.get_type().name()?.to_string(),
));
};
Ok(Features {
features,
entity_type: EntityType::Feature,
})
}
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
impl Features {
pub fn iter(&self) -> std::slice::Iter<'_, Feature> {
self.features.iter()
}
}
#[pyclass]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct FeatureMap {
#[pyo3(get)]
pub features: HashMap<String, HashMap<String, i32>>,
}
#[pymethods]
impl FeatureMap {
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
#[pyclass]
#[derive(Clone, Serialize, Debug)]
pub struct Metric {
pub name: String,
pub value: f64,
}
#[pymethods]
impl Metric {
#[new]
pub fn new(name: String, value: Bound<'_, PyAny>) -> Self {
let value = if value.is_instance_of::<PyFloat>() {
value.extract::<f64>().unwrap()
} else if value.is_instance_of::<PyInt>() {
value.extract::<i64>().unwrap() as f64
} else {
panic!(
"Unsupported metric type: {}",
value.get_type().name().unwrap()
);
};
let lowercase_name = name.to_lowercase();
Metric {
name: lowercase_name,
value,
}
}
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
impl Metric {
pub fn new_rs(name: String, value: f64) -> Self {
Metric { name, value }
}
}
#[pyclass]
#[derive(Clone, Serialize, Debug)]
pub struct Metrics {
#[pyo3(get)]
pub metrics: Vec<Metric>,
#[pyo3(get)]
pub entity_type: EntityType,
}
#[pymethods]
impl Metrics {
#[new]
pub fn new(metrics: Bound<'_, PyAny>) -> Result<Self, TypeError> {
let metrics = if metrics.is_instance_of::<PyList>() {
let list = metrics.cast::<PyList>()?;
let mut result = Vec::with_capacity(list.len());
for item in list.iter() {
result.push(item.extract::<Metric>()?);
}
result
} else if metrics.is_instance_of::<PyDict>() {
let dict = metrics.cast::<PyDict>()?;
let mut result = Vec::with_capacity(dict.len());
for (key, value) in dict.iter() {
let name = key.extract::<String>()?;
result.push(Metric::new(name, value));
}
result
} else {
return Err(TypeError::UnsupportedMetricsTypeError(
metrics.get_type().name()?.to_string(),
));
};
Ok(Metrics {
metrics,
entity_type: EntityType::Metric,
})
}
pub fn __str__(&self) -> String {
PyHelperFuncs::__str__(self)
}
}
impl Metrics {
pub fn iter(&self) -> std::slice::Iter<'_, Metric> {
self.metrics.iter()
}
}
#[derive(Debug)]
pub enum QueueItem {
Features(Features),
Metrics(Metrics),
GenAI(Box<EvalRecord>),
}
impl QueueItem {
pub fn from_py_entity(entity: &Bound<'_, PyAny>) -> Result<Self, TypeError> {
let entity_type = entity
.getattr("entity_type")?
.extract::<EntityType>()
.inspect_err(|e| error!("Failed to extract entity type: {}", e))?;
match entity_type {
EntityType::Feature => {
let features = entity.extract::<Features>()?;
Ok(QueueItem::Features(features))
}
EntityType::Metric => {
let metrics = entity.extract::<Metrics>()?;
Ok(QueueItem::Metrics(metrics))
}
EntityType::GenAI => {
let genai = entity.extract::<EvalRecord>()?;
Ok(QueueItem::GenAI(Box::new(genai)))
}
}
}
}
pub trait QueueExt: Send + Sync {
fn metrics(&self) -> &Vec<Metric>;
fn features(&self) -> &Vec<Feature>;
fn into_genai_record(self) -> Option<EvalRecord>;
}
impl QueueExt for Features {
fn metrics(&self) -> &Vec<Metric> {
static EMPTY: Vec<Metric> = Vec::new();
&EMPTY
}
fn features(&self) -> &Vec<Feature> {
&self.features
}
fn into_genai_record(self) -> Option<EvalRecord> {
None
}
}
impl QueueExt for Metrics {
fn metrics(&self) -> &Vec<Metric> {
&self.metrics
}
fn features(&self) -> &Vec<Feature> {
static EMPTY: Vec<Feature> = Vec::new();
&EMPTY
}
fn into_genai_record(self) -> Option<EvalRecord> {
None
}
}
impl QueueExt for EvalRecord {
fn metrics(&self) -> &Vec<Metric> {
static EMPTY: Vec<Metric> = Vec::new();
&EMPTY
}
fn features(&self) -> &Vec<Feature> {
static EMPTY: Vec<Feature> = Vec::new();
&EMPTY
}
fn into_genai_record(self) -> Option<EvalRecord> {
Some(self)
}
}