use std::collections::BTreeMap;
use std::str::FromStr;
use http::header::HeaderMap;
use indexmap::IndexMap;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use serde_json::value::RawValue;
use crate::constants::REQUEST_ID_HEADER;
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct NoulAnswer {
pub noul: f64,
}
impl NoulAnswer {
pub fn is_yes(&self, threshold: f64) -> bool {
self.noul >= threshold
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct ChoiceAnswer {
pub choice: String,
pub probabilities: IndexMap<String, f64>,
pub confidence: f64,
}
impl ChoiceAnswer {
pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
self.choice.parse()
}
pub fn probability(&self, label: &str) -> Option<f64> {
self.probabilities.get(label).copied()
}
pub fn ranked(&self) -> Vec<(&str, f64)> {
let mut v: Vec<_> = self
.probabilities
.iter()
.map(|(k, p)| (k.as_str(), *p))
.collect();
v.sort_by(|a, b| b.1.total_cmp(&a.1));
v
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct ScoreAnswer {
pub score: f64,
pub confidence: f64,
pub legend: BTreeMap<u32, Value>,
pub probabilities: BTreeMap<u32, f64>,
}
impl ScoreAnswer {
pub fn most_likely_level(&self) -> Option<u32> {
self.probabilities
.iter()
.max_by(|a, b| a.1.total_cmp(b.1))
.map(|(level, _)| *level)
}
pub fn rounded_level(&self) -> u32 {
self.score.round().max(0.0) as u32
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Answer {
Noul(NoulAnswer),
Choice(ChoiceAnswer),
Score(ScoreAnswer),
}
impl Answer {
pub fn kind(&self) -> &'static str {
match self {
Answer::Noul(_) => "noul",
Answer::Choice(_) => "choice",
Answer::Score(_) => "score",
}
}
pub fn confidence(&self) -> Option<f64> {
match self {
Answer::Noul(_) => None,
Answer::Choice(a) => Some(a.confidence),
Answer::Score(a) => Some(a.confidence),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[non_exhaustive]
pub struct Usage {
#[serde(default)]
pub input_tokens: Option<u64>,
#[serde(default)]
pub output_tokens: Option<u64>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ResponseMeta {
pub status: u16,
pub headers: HeaderMap,
pub attempts: u32,
}
impl ResponseMeta {
pub fn request_id(&self) -> Option<&str> {
self.headers
.get(REQUEST_ID_HEADER)
.and_then(|v| v.to_str().ok())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SystemOneResponse {
pub model: String,
pub usage: Usage,
pub answers: IndexMap<String, Answer>,
pub raw: Value,
pub meta: ResponseMeta,
}
impl SystemOneResponse {
pub fn request_id(&self) -> Option<&str> {
self.meta.request_id()
}
pub fn noul(&self, name: &str) -> Option<&NoulAnswer> {
match self.answers.get(name)? {
Answer::Noul(a) => Some(a),
_ => None,
}
}
pub fn choice(&self, name: &str) -> Option<&ChoiceAnswer> {
match self.answers.get(name)? {
Answer::Choice(a) => Some(a),
_ => None,
}
}
pub fn score(&self, name: &str) -> Option<&ScoreAnswer> {
match self.answers.get(name)? {
Answer::Score(a) => Some(a),
_ => None,
}
}
pub fn nouls(&self) -> impl Iterator<Item = (&str, &NoulAnswer)> {
self.answers.iter().filter_map(|(k, a)| match a {
Answer::Noul(n) => Some((k.as_str(), n)),
_ => None,
})
}
pub fn choices(&self) -> impl Iterator<Item = (&str, &ChoiceAnswer)> {
self.answers.iter().filter_map(|(k, a)| match a {
Answer::Choice(c) => Some((k.as_str(), c)),
_ => None,
})
}
pub fn scores(&self) -> impl Iterator<Item = (&str, &ScoreAnswer)> {
self.answers.iter().filter_map(|(k, a)| match a {
Answer::Score(s) => Some((k.as_str(), s)),
_ => None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[non_exhaustive]
pub struct ModelMetadata {
pub name: String,
pub description: String,
pub release_date: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ListModelsResponse {
pub models: Vec<ModelMetadata>,
pub raw: Value,
pub meta: ResponseMeta,
}
pub(crate) struct DecodeFailure {
pub path: String,
pub detail: String,
}
fn typed<T: DeserializeOwned>(prefix: &str, json: &[u8]) -> Result<T, DecodeFailure> {
let mut de = serde_json::Deserializer::from_slice(json);
let value = serde_path_to_error::deserialize(&mut de).map_err(|e| {
let inner = e.path().to_string();
let path = match (prefix.is_empty(), inner.as_str()) {
(true, ".") => String::new(),
(true, _) => inner.clone(),
(false, ".") => prefix.to_owned(),
(false, _) => format!("{prefix}.{inner}"),
};
let msg = e.inner().to_string();
let path = match msg
.strip_prefix("missing field `")
.and_then(|r| r.split('`').next())
{
Some(field) if path.is_empty() => field.to_owned(),
Some(field) => format!("{path}.{field}"),
None => path,
};
DecodeFailure { path, detail: msg }
})?;
de.end().map_err(|e| DecodeFailure {
path: prefix.to_owned(),
detail: e.to_string(),
})?;
Ok(value)
}
#[derive(Deserialize)]
struct Envelope {
model: String,
#[serde(default)]
usage: Usage,
answers: IndexMap<String, Box<RawValue>>,
}
pub(crate) struct DecodedSystemOne {
pub model: String,
pub usage: Usage,
pub answers: IndexMap<String, Answer>,
pub raw: Value,
}
pub(crate) fn decode_system_one(body: &[u8]) -> Result<DecodedSystemOne, DecodeFailure> {
let env: Envelope = typed("", body)?;
let mut answers = IndexMap::with_capacity(env.answers.len());
for (name, value) in env.answers {
let prefix = format!("answers.{name}");
let json = value.get();
let tag = serde_json::from_str::<Value>(json)
.ok()
.and_then(|v| v.get("type").and_then(Value::as_str).map(str::to_owned));
let Some(tag) = tag else {
return Err(DecodeFailure {
path: format!("{prefix}.type"),
detail: "missing or non-string answer type".into(),
});
};
let answer = match tag.as_str() {
"noul" => Answer::Noul(typed(&prefix, json.as_bytes())?),
"choice" => Answer::Choice(typed(&prefix, json.as_bytes())?),
"score" => Answer::Score(typed(&prefix, json.as_bytes())?),
other => {
tracing::warn!(answer = %name, r#type = %other, "ignoring answer with unrecognized type");
continue;
}
};
answers.insert(name, answer);
}
let raw = serde_json::from_slice(body).unwrap_or(Value::Null);
Ok(DecodedSystemOne {
model: env.model,
usage: env.usage,
answers,
raw,
})
}
#[derive(Deserialize)]
struct ModelList {
models: Vec<ModelMetadata>,
}
pub(crate) fn decode_models(body: &[u8]) -> Result<(Vec<ModelMetadata>, Value), DecodeFailure> {
let list: ModelList = typed("", body)?;
let raw = serde_json::from_slice(body).unwrap_or(Value::Null);
Ok((list.models, raw))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample() -> Value {
json!({
"model": "jev-latest",
"answers": {
"department": {"type": "choice", "choice": "technical",
"probabilities": {"billing": 0.159, "technical": 0.84, "sales": 0.001}, "confidence": 0.596},
"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},
"is_urgent": {"type": "noul", "noul": 0.999},
"future": {"type": "span", "start": 3}
},
"usage": {"input_tokens": 312, "output_tokens": 48, "extra": true}
})
}
fn decode(v: Value) -> Result<DecodedSystemOne, DecodeFailure> {
decode_system_one(&serde_json::to_vec(&v).unwrap())
}
#[test]
fn decodes_all_types_and_skips_unknown() {
let DecodedSystemOne {
model,
usage,
answers,
raw,
} = decode(sample()).ok().unwrap();
assert_eq!(model, "jev-latest");
assert_eq!(usage.input_tokens, Some(312));
assert_eq!(answers.len(), 3);
assert_eq!(raw["answers"]["future"]["start"], json!(3));
let Answer::Score(s) = &answers["frustration"] else {
panic!()
};
assert_eq!(s.legend[&2], json!("Very angry"));
assert_eq!(s.most_likely_level(), Some(2));
assert_eq!(s.rounded_level(), 2);
let Answer::Choice(c) = &answers["department"] else {
panic!()
};
assert_eq!(c.ranked()[0], ("technical", 0.84));
}
#[test]
fn preserves_server_order_of_probabilities() {
let body = br#"{"model":"m","usage":{},"answers":{"c":{"type":"choice","choice":"z",
"probabilities":{"z":0.5,"a":0.3,"m":0.2},"confidence":0.1}}}"#;
let d = decode_system_one(body).ok().unwrap();
let Answer::Choice(c) = &d.answers["c"] else {
panic!()
};
let keys: Vec<_> = c.probabilities.keys().map(String::as_str).collect();
assert_eq!(keys, ["z", "a", "m"]);
}
#[test]
fn usage_may_be_empty_or_absent() {
let d = decode(json!({"model": "m", "answers": {}, "usage": {}}))
.ok()
.unwrap();
assert_eq!(d.usage, Usage::default());
let d = decode(json!({"model": "m", "answers": {}})).ok().unwrap();
assert_eq!(d.usage, Usage::default());
}
#[test]
fn rejects_non_json_and_trailing_content() {
assert_eq!(decode_system_one(b"<html>").err().unwrap().path, "");
let mut body = serde_json::to_vec(&sample()).unwrap();
body.extend_from_slice(b" trailing");
assert!(decode_system_one(&body).is_err());
}
#[test]
fn reports_precise_paths() {
let mut v = sample();
v["answers"]["department"]
.as_object_mut()
.unwrap()
.remove("confidence");
assert_eq!(
decode(v).err().unwrap().path,
"answers.department.confidence"
);
let mut v = sample();
v["answers"]["frustration"]["probabilities"]["1"] = json!("high");
assert_eq!(
decode(v).err().unwrap().path,
"answers.frustration.probabilities.1"
);
let mut v = sample();
v["answers"]["is_urgent"]["type"] = json!(7);
assert_eq!(decode(v).err().unwrap().path, "answers.is_urgent.type");
let mut v = sample();
v.as_object_mut().unwrap().remove("model");
assert_eq!(decode(v).err().unwrap().path, "model");
}
}