use http::{HeaderMap, StatusCode};
use indexmap::IndexMap;
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
}
#[derive(Clone, Debug, Default)]
pub struct ResponseMeta {
pub request_id: Option<String>,
pub status: Option<StatusCode>,
pub headers: HeaderMap,
pub attempts: u32,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[non_exhaustive]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Answer {
Noul {
noul: f64,
},
Choice {
choice: String,
probabilities: IndexMap<String, f64>,
confidence: f64,
},
Score {
score: f64,
legend: IndexMap<String, serde_json::Value>,
#[serde(default)]
probabilities: Option<IndexMap<String, f64>>,
confidence: f64,
},
#[serde(other)]
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NoulView {
pub noul: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ChoiceView<'a> {
pub choice: &'a str,
pub probabilities: &'a IndexMap<String, f64>,
pub confidence: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScoreView<'a> {
pub score: f64,
pub legend: &'a IndexMap<String, serde_json::Value>,
pub probabilities: Option<&'a IndexMap<String, f64>>,
pub confidence: f64,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SystemOneResponse {
pub model: String,
pub answers: IndexMap<String, Answer>,
#[serde(default)]
pub usage: Option<Usage>,
#[serde(skip)]
pub meta: ResponseMeta,
}
impl SystemOneResponse {
pub fn nouls(&self) -> impl Iterator<Item = (&str, NoulView)> + '_ {
self.answers
.iter()
.filter_map(|(key, answer)| match answer {
Answer::Noul { noul } => Some((key.as_str(), NoulView { noul: *noul })),
_ => None,
})
}
pub fn choices(&self) -> impl Iterator<Item = (&str, ChoiceView<'_>)> + '_ {
self.answers
.iter()
.filter_map(|(key, answer)| match answer {
Answer::Choice {
choice,
probabilities,
confidence,
} => Some((
key.as_str(),
ChoiceView {
choice,
probabilities,
confidence: *confidence,
},
)),
_ => None,
})
}
pub fn scores(&self) -> impl Iterator<Item = (&str, ScoreView<'_>)> + '_ {
self.answers
.iter()
.filter_map(|(key, answer)| match answer {
Answer::Score {
score,
legend,
probabilities,
confidence,
} => Some((
key.as_str(),
ScoreView {
score: *score,
legend,
probabilities: probabilities.as_ref(),
confidence: *confidence,
},
)),
_ => None,
})
}
#[must_use]
pub fn noul(&self, key: &str) -> Option<f64> {
match self.answers.get(key) {
Some(Answer::Noul { noul }) => Some(*noul),
_ => None,
}
}
#[must_use]
pub fn choice(&self, key: &str) -> Option<ChoiceView<'_>> {
match self.answers.get(key) {
Some(Answer::Choice {
choice,
probabilities,
confidence,
}) => Some(ChoiceView {
choice,
probabilities,
confidence: *confidence,
}),
_ => None,
}
}
#[must_use]
pub fn score(&self, key: &str) -> Option<ScoreView<'_>> {
match self.answers.get(key) {
Some(Answer::Score {
score,
legend,
probabilities,
confidence,
}) => Some(ScoreView {
score: *score,
legend,
probabilities: probabilities.as_ref(),
confidence: *confidence,
}),
_ => None,
}
}
#[must_use]
pub fn answer(&self, key: &str) -> Option<&Answer> {
self.answers.get(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn deserializes_noul_choice_score() {
let body = json!({
"model": "jev-latest",
"answers": {
"urgent": { "type": "noul", "noul": 0.92 },
"dept": {
"type": "choice",
"choice": "technical",
"probabilities": { "billing": 0.08, "technical": 0.85, "sales": 0.07 },
"confidence": 0.82
},
"frustration": {
"type": "score",
"score": 1.6,
"legend": { "0": "Calm", "1": "Frustrated", "2": "Very angry" },
"probabilities": { "0": 0.05, "1": 0.3, "2": 0.65 },
"confidence": 0.78
}
},
"usage": { "input_tokens": 312, "output_tokens": 48 }
});
let resp: SystemOneResponse = serde_json::from_value(body).unwrap();
assert_eq!(resp.noul("urgent"), Some(0.92));
assert_eq!(resp.choice("dept").unwrap().choice, "technical");
assert_eq!(resp.score("frustration").unwrap().score, 1.6);
assert_eq!(resp.usage.as_ref().unwrap().input_tokens, 312);
assert_eq!(resp.nouls().count(), 1);
assert_eq!(resp.choices().count(), 1);
assert_eq!(resp.scores().count(), 1);
}
#[test]
fn unknown_answer_type_does_not_fail() {
let body = json!({
"model": "jev-latest",
"answers": {
"future": { "type": "spectrum", "value": 1 },
"urgent": { "type": "noul", "noul": 0.5 }
}
});
let resp: SystemOneResponse = serde_json::from_value(body).unwrap();
assert!(matches!(resp.answer("future"), Some(Answer::Unknown)));
assert_eq!(resp.noul("urgent"), Some(0.5));
}
#[test]
fn extra_fields_on_known_answers_are_ignored() {
let body = json!({
"model": "jev-latest",
"answers": {
"urgent": { "type": "noul", "noul": 0.1, "extra": true }
},
"bonus": 1
});
let resp: SystemOneResponse = serde_json::from_value(body).unwrap();
assert_eq!(resp.noul("urgent"), Some(0.1));
}
#[test]
fn score_without_probabilities_is_ok() {
let body = json!({
"model": "jev-latest",
"answers": {
"s": {
"type": "score",
"score": 0.0,
"legend": { "0": "a", "1": "b" },
"confidence": 0.5
}
}
});
let resp: SystemOneResponse = serde_json::from_value(body).unwrap();
assert!(resp.score("s").unwrap().probabilities.is_none());
}
}