use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SystemOneResponse {
pub model: String,
pub answers: IndexMap<String, Answer>,
pub usage: Usage,
}
impl SystemOneResponse {
pub fn get(&self, id: &str) -> Option<&Answer> {
self.answers.get(id)
}
pub fn nouls(&self) -> impl Iterator<Item = (&str, &NoulAnswer)> {
self.answers.iter().filter_map(|(k, v)| match v {
Answer::Noul(a) => Some((k.as_str(), a)),
_ => None,
})
}
pub fn choices(&self) -> impl Iterator<Item = (&str, &ChoiceAnswer)> {
self.answers.iter().filter_map(|(k, v)| match v {
Answer::Choice(a) => Some((k.as_str(), a)),
_ => None,
})
}
pub fn scores(&self) -> impl Iterator<Item = (&str, &ScoreAnswer)> {
self.answers.iter().filter_map(|(k, v)| match v {
Answer::Score(a) => Some((k.as_str(), a)),
_ => None,
})
}
pub fn noul(&self, id: &str) -> Option<&NoulAnswer> {
self.answers.get(id).and_then(Answer::as_noul)
}
pub fn choice(&self, id: &str) -> Option<&ChoiceAnswer> {
self.answers.get(id).and_then(Answer::as_choice)
}
pub fn score(&self, id: &str) -> Option<&ScoreAnswer> {
self.answers.get(id).and_then(Answer::as_score)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Answer {
Noul(NoulAnswer),
Choice(ChoiceAnswer),
Score(ScoreAnswer),
}
impl Answer {
pub fn as_noul(&self) -> Option<&NoulAnswer> {
match self {
Answer::Noul(a) => Some(a),
_ => None,
}
}
pub fn as_choice(&self) -> Option<&ChoiceAnswer> {
match self {
Answer::Choice(a) => Some(a),
_ => None,
}
}
pub fn as_score(&self) -> Option<&ScoreAnswer> {
match self {
Answer::Score(a) => Some(a),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct NoulAnswer {
pub noul: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChoiceAnswer {
pub choice: String,
#[serde(default)]
pub probabilities: IndexMap<String, f64>,
pub confidence: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScoreAnswer {
pub score: f64,
#[serde(default)]
pub legend: IndexMap<String, String>,
#[serde(default)]
pub probabilities: IndexMap<String, f64>,
pub confidence: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_full_response() {
let raw = json!({
"model": "jev-1.13.0",
"answers": {
"department": {
"type": "choice",
"choice": "technical",
"probabilities": {"billing": 0.159, "technical": 0.84, "sales": 0.001},
"confidence": 0.596,
},
"frustration": {
"type": "score",
"score": 1.035,
"legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
"probabilities": {"0": 0.1, "1": 0.6, "2": 0.3},
"confidence": 0.842,
},
"is_urgent": {"type": "noul", "noul": 0.999},
},
"usage": {"input_tokens": 312, "output_tokens": 48},
});
let resp: SystemOneResponse = serde_json::from_value(raw).unwrap();
assert_eq!(resp.model, "jev-1.13.0");
assert_eq!(resp.answers.len(), 3);
assert_eq!(resp.usage.input_tokens, 312);
let dept = resp.choice("department").unwrap();
assert_eq!(dept.choice, "technical");
assert_eq!(dept.probabilities["billing"], 0.159);
assert_eq!(dept.confidence, 0.596);
let fr = resp.score("frustration").unwrap();
assert_eq!(fr.score, 1.035);
assert_eq!(fr.legend["2"], "Very angry");
assert_eq!(fr.probabilities["1"], 0.6);
let urg = resp.noul("is_urgent").unwrap();
assert_eq!(urg.noul, 0.999);
assert_eq!(resp.nouls().count(), 1);
assert_eq!(resp.choices().count(), 1);
assert_eq!(resp.scores().count(), 1);
}
#[test]
fn score_answer_tolerates_missing_probabilities() {
let raw = json!({
"model": "jev-latest",
"answers": {
"s": {"type": "score", "score": 0.0, "confidence": 1.0, "legend": {"0": "only"}}
},
"usage": {"input_tokens": 1, "output_tokens": 1},
});
let resp: SystemOneResponse = serde_json::from_value(raw).unwrap();
assert!(resp.score("s").unwrap().probabilities.is_empty());
}
#[test]
fn responses_round_trip_through_json() {
let raw = json!({
"model": "jev-1.13.0",
"answers": {
"ok": {"type": "noul", "noul": 0.87},
"path": {
"type": "choice",
"choice": "a",
"probabilities": {"a": 0.9, "b": 0.1},
"confidence": 0.8,
},
},
"usage": {"input_tokens": 10, "output_tokens": 4},
});
let resp: SystemOneResponse = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&resp).unwrap(), raw);
}
}