use std::collections::BTreeMap;
use ironflow_core::decision::{DecisionModel, DecisionQuestion, DecisionRequest, NoulCriteria};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DEFAULT_DECISION_MODEL: &str = DecisionModel::LATEST;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionConfig {
pub state: Value,
#[serde(default)]
pub model: DecisionModel,
#[serde(default)]
pub questions: BTreeMap<String, DecisionQuestion>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub escalate_below: Option<f64>,
}
impl DecisionConfig {
pub fn new(state: impl Serialize) -> Self {
Self {
state: serde_json::to_value(state).unwrap_or(Value::Null),
model: DecisionModel::default(),
questions: BTreeMap::new(),
escalate_below: None,
}
}
pub fn model(mut self, model: impl Into<DecisionModel>) -> Self {
self.model = model.into();
self
}
pub fn noul(self, name: &str, instructions: impl Serialize) -> Self {
self.noul_with(name, instructions, NoulCriteria::default())
}
pub fn noul_with(
mut self,
name: &str,
instructions: impl Serialize,
criteria: NoulCriteria,
) -> Self {
self.questions.insert(
name.to_string(),
DecisionQuestion::Noul {
instructions: to_value(instructions),
criteria,
},
);
self
}
pub fn choice(self, name: &str, instructions: impl Serialize, options: &[&str]) -> Self {
let described: Vec<(&str, Option<&str>)> = options.iter().map(|o| (*o, None)).collect();
self.choice_described(name, instructions, &described)
}
pub fn choice_described(
mut self,
name: &str,
instructions: impl Serialize,
options: &[(&str, Option<&str>)],
) -> Self {
let criteria = options
.iter()
.map(|(label, desc)| (label.to_string(), desc.map(str::to_string)))
.collect();
self.questions.insert(
name.to_string(),
DecisionQuestion::Choice {
instructions: to_value(instructions),
criteria,
},
);
self
}
pub fn score(mut self, name: &str, instructions: impl Serialize, levels: &[&str]) -> Self {
self.questions.insert(
name.to_string(),
DecisionQuestion::Score {
instructions: to_value(instructions),
criteria: levels.iter().map(|l| l.to_string()).collect(),
},
);
self
}
pub fn escalate_below(mut self, threshold: f64) -> Self {
self.escalate_below = Some(threshold);
self
}
pub fn to_request(&self) -> DecisionRequest {
DecisionRequest {
state: self.state.clone(),
model: self.model.clone(),
questions: self.questions.clone(),
}
}
}
fn to_value(value: impl Serialize) -> Value {
serde_json::to_value(value).unwrap_or(Value::Null)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_assembles_questions() {
let config = DecisionConfig::new("state")
.noul("a", "yes/no?")
.choice("b", "pick", &["x", "y"])
.score("c", "rate", &["low", "high"])
.escalate_below(0.6);
assert_eq!(config.questions.len(), 3);
assert_eq!(config.escalate_below, Some(0.6));
assert_eq!(config.model, "jev-latest");
}
#[test]
fn to_request_carries_state_and_questions() {
let config = DecisionConfig::new("hello").noul("a", "?");
let request = config.to_request();
assert_eq!(request.state, serde_json::json!("hello"));
assert_eq!(request.questions.len(), 1);
}
#[test]
fn decision_config_serde_roundtrip() {
let config = DecisionConfig::new("s")
.noul("a", "?")
.choice("b", "?", &["x"])
.escalate_below(0.5);
let json = serde_json::to_string(&config).unwrap();
let back: DecisionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.questions.len(), 2);
assert_eq!(back.escalate_below, Some(0.5));
}
#[test]
fn model_defaults_when_missing_in_json() {
let config: DecisionConfig = serde_json::from_str(r#"{"state":"s"}"#).unwrap();
assert_eq!(config.model, "jev-latest");
assert!(config.questions.is_empty());
}
}