use std::sync::Arc;
use crate::hashmap::*;
use chrono::{DateTime, Utc};
use crate::eval::get_assignment;
use crate::precomputed::{PrecomputedAssignment, PrecomputedBandit, PrecomputedConfiguration};
use crate::ufc::{ConfigurationFormat, ValueWire, VariationType};
use crate::{Configuration, ContextAttributes, Str};
pub fn get_precomputed_configuration(
configuration: Option<&Configuration>,
subject_key: &Str,
subject_attributes: &Arc<ContextAttributes>,
flag_actions: &HashMap<
Str,
HashMap< Str, ContextAttributes>,
>,
now: DateTime<Utc>,
) -> PrecomputedConfiguration {
let Some(configuration) = configuration else {
log::warn!(target: "eppo",
subject_key;
"evaluating a flag before Eppo configuration has been fetched");
return PrecomputedConfiguration {
obfuscated: serde_bool::False,
format: ConfigurationFormat::Precomputed,
created_at: now,
environment: None,
flags: HashMap::new(),
bandits: HashMap::new(),
};
};
let generic_attributes = Arc::new(subject_attributes.to_generic_attributes());
let flags = configuration
.flags
.compiled
.flags
.keys()
.filter_map(|flag_key| {
get_assignment(
Some(configuration),
flag_key,
&subject_key,
&generic_attributes,
None,
now,
)
.unwrap_or_else(|err| {
log::warn!(
target: "eppo",
subject_key,
flag_key,
err:?;
"Failed to evaluate assignment"
);
None
})
.map(|assignment| (flag_key.clone(), PrecomputedAssignment::from(assignment)))
})
.collect::<HashMap<_, _>>();
let bandits = configuration
.bandits
.as_ref()
.map(|bandits| {
flag_actions
.iter()
.filter_map(|(flag_key, actions)| {
let flag = flags.get(flag_key)?;
if flag.variation_type != VariationType::String {
return None;
}
let Some(ValueWire::String(precomputed_variation_value)) = flags
.get(flag_key)
.map(|assignment| &assignment.variation_value)
else {
return None;
};
let bandit_key = &configuration
.flags
.compiled
.flag_to_bandit_associations
.get(flag_key)?
.get(precomputed_variation_value)?
.key;
let bandit_model = bandits.bandits.get(bandit_key)?;
let bandit_evaluation = bandit_model
.model_data
.evaluate(
flag_key,
subject_key,
subject_attributes,
actions.iter(),
configuration.bandit_hashing_algorithm,
)
.ok()?;
let selected_action = &actions[&bandit_evaluation.action_key];
let precomputed_bandit = PrecomputedBandit {
bandit_key: bandit_key.clone(),
action: bandit_evaluation.action_key,
action_probability: bandit_evaluation.action_weight,
optimality_gap: bandit_evaluation.optimality_gap,
model_version: bandit_model.model_version.clone(),
action_numeric_attributes: selected_action.numeric.clone(),
action_categorical_attributes: selected_action.categorical.clone(),
};
Some((flag_key.clone(), precomputed_bandit))
})
.collect()
})
.unwrap_or_default();
let result = PrecomputedConfiguration {
obfuscated: serde_bool::False,
created_at: now,
format: ConfigurationFormat::Precomputed,
environment: Some(configuration.flags.compiled.environment.clone()),
flags,
bandits,
};
log::trace!(
target: "eppo",
subject_key,
configuration:serde = result;
"evaluated precomputed assignments");
result
}
#[cfg(test)]
mod tests {
use crate::Str;
use chrono::Utc;
use crate::{
eval::get_precomputed_configuration, ufc::UniversalFlagConfig, Configuration,
ContextAttributes, SdkMetadata,
};
use std::{fs, sync::Arc};
use crate::hashmap::*;
#[test]
fn test_precomputed_assignment_basic() {
let _ = env_logger::builder().is_test(true).try_init();
let configuration = {
let ufc_config = UniversalFlagConfig::from_json(
SdkMetadata {
name: "test",
version: "0.1.0",
},
fs::read("../sdk-test-data/ufc/flags-v1.json").unwrap(),
)
.unwrap();
Configuration::from_server_response(ufc_config, None)
};
let subject_key = "test-subject-1".into();
let subject_attributes = Default::default();
let actions = Default::default();
let now = Utc::now();
let precomputed = get_precomputed_configuration(
Some(&configuration),
&subject_key,
&subject_attributes,
&actions,
now,
);
assert!(
!precomputed.flags.is_empty(),
"Should have precomputed flags"
);
for flag_key in precomputed.flags.keys() {
assert!(
precomputed.flags.contains_key(flag_key),
"Should have precomputed assignment for flag {}",
flag_key
);
}
}
#[test]
fn test_precomputed_assignment_bandits() {
let _ = env_logger::builder().is_test(true).try_init();
let configuration = {
let ufc_config = UniversalFlagConfig::from_json(
SdkMetadata {
name: "test",
version: "0.1.0",
},
fs::read("../sdk-test-data/ufc/bandit-flags-v1.json").unwrap(),
)
.unwrap();
let bandits_config = serde_json::from_slice(
&fs::read("../sdk-test-data/ufc/bandit-models-v1.json").unwrap(),
)
.unwrap();
Configuration::from_server_response(ufc_config, Some(bandits_config))
};
let subject_key = "test-subject-1".into();
let subject_attributes = Default::default();
let mut actions = HashMap::new();
let car_actions: HashMap<Str, ContextAttributes> = [
("dodge".into(), Default::default()),
("mercedes".into(), Default::default()),
(
"toyota".into(),
ContextAttributes {
numeric: Arc::new([("speed".into(), (1000.0).into())].into_iter().collect()),
categorical: Default::default(),
},
),
]
.into_iter()
.collect();
actions.insert("car_bandit_flag".into(), car_actions);
let now = Utc::now();
let precomputed = get_precomputed_configuration(
Some(&configuration),
&subject_key,
&subject_attributes,
&actions,
now,
);
assert!(
!precomputed.flags.is_empty(),
"Should have precomputed flags"
);
for flag_key in precomputed.flags.keys() {
assert!(
precomputed.flags.contains_key(flag_key),
"Should have precomputed assignment for flag {}",
flag_key
);
}
}
#[test]
fn test_precomputed_assignment_with_and_without_actions() {
let _ = env_logger::builder().is_test(true).try_init();
let configuration = {
let ufc_config = UniversalFlagConfig::from_json(
SdkMetadata {
name: "test",
version: "0.1.0",
},
fs::read("../sdk-test-data/ufc/bandit-flags-v1.json").unwrap(),
)
.unwrap();
let bandits_config = serde_json::from_slice(
&fs::read("../sdk-test-data/ufc/bandit-models-v1.json").unwrap(),
)
.unwrap();
Configuration::from_server_response(ufc_config, Some(bandits_config))
};
let subject_key = "test-subject-1".into();
let subject_attributes = Arc::new(ContextAttributes {
numeric: Default::default(),
categorical: Arc::new([("country".into(), "UK".into())].into_iter().collect()),
});
let now = Utc::now();
let precomputed_no_actions = get_precomputed_configuration(
Some(&configuration),
&subject_key,
&subject_attributes,
&HashMap::new(),
now,
);
assert!(
precomputed_no_actions.bandits.is_empty(),
"Should have no bandit data when no actions provided"
);
let mut car_only_actions = HashMap::new();
let car_actions: HashMap<Str, ContextAttributes> = [(
"toyota".into(),
ContextAttributes {
numeric: Arc::new([("speed".into(), (1000.0).into())].into_iter().collect()),
categorical: Default::default(),
},
)]
.into_iter()
.collect();
car_only_actions.insert("car_bandit_flag".into(), car_actions);
let precomputed_car_only = get_precomputed_configuration(
Some(&configuration),
&subject_key,
&subject_attributes,
&car_only_actions,
now,
);
assert!(
precomputed_car_only.bandits.contains_key("car_bandit_flag"),
"Should have car bandit data when car actions provided"
);
assert!(
!precomputed_car_only
.bandits
.contains_key("banner_bandit_flag"),
"Should not have banner bandit data when only car actions provided"
);
assert!(
!precomputed_car_only
.bandits
.contains_key("banner_bandit_flag_uk_only"),
"Should not have UK banner bandit data when only car actions provided"
);
let mut banner_only_actions = HashMap::new();
let banner_actions: HashMap<Str, ContextAttributes> = [
("nike".into(), Default::default()),
("adidas".into(), Default::default()),
]
.into_iter()
.collect();
banner_only_actions.insert("banner_bandit_flag".into(), banner_actions.clone());
banner_only_actions.insert("banner_bandit_flag_uk_only".into(), banner_actions);
let precomputed_banner_only = get_precomputed_configuration(
Some(&configuration),
&subject_key,
&subject_attributes,
&banner_only_actions,
now,
);
assert!(
!precomputed_banner_only
.bandits
.contains_key("car_bandit_flag"),
"Should not have car bandit data when only banner actions provided"
);
assert!(
precomputed_banner_only
.bandits
.contains_key("banner_bandit_flag"),
"Should have banner bandit data when banner actions provided"
);
assert!(
precomputed_banner_only
.bandits
.contains_key("banner_bandit_flag_uk_only"),
"Should have UK banner bandit data when banner actions provided"
);
}
}