1use std::collections::BTreeMap;
4use std::str::FromStr;
5
6use http::header::HeaderMap;
7use indexmap::IndexMap;
8use serde::Deserialize;
9use serde::de::DeserializeOwned;
10use serde_json::Value;
11use serde_json::value::RawValue;
12
13use crate::constants::REQUEST_ID_HEADER;
14
15#[derive(Debug, Clone, PartialEq, Deserialize)]
17#[non_exhaustive]
18pub struct NoulAnswer {
19 pub noul: f64,
21}
22
23impl NoulAnswer {
24 pub fn is_yes(&self, threshold: f64) -> bool {
26 self.noul >= threshold
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Deserialize)]
32#[non_exhaustive]
33pub struct ChoiceAnswer {
34 pub choice: String,
36 pub probabilities: IndexMap<String, f64>,
38 pub confidence: f64,
40}
41
42impl ChoiceAnswer {
43 pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
45 self.choice.parse()
46 }
47
48 pub fn probability(&self, label: &str) -> Option<f64> {
50 self.probabilities.get(label).copied()
51 }
52
53 pub fn ranked(&self) -> Vec<(&str, f64)> {
55 let mut v: Vec<_> = self
56 .probabilities
57 .iter()
58 .map(|(k, p)| (k.as_str(), *p))
59 .collect();
60 v.sort_by(|a, b| b.1.total_cmp(&a.1));
61 v
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Deserialize)]
67#[non_exhaustive]
68pub struct ScoreAnswer {
69 pub score: f64,
71 pub confidence: f64,
73 pub legend: BTreeMap<u32, Value>,
75 pub probabilities: BTreeMap<u32, f64>,
77}
78
79impl ScoreAnswer {
80 pub fn most_likely_level(&self) -> Option<u32> {
82 self.probabilities
83 .iter()
84 .max_by(|a, b| a.1.total_cmp(b.1))
85 .map(|(level, _)| *level)
86 }
87
88 pub fn rounded_level(&self) -> u32 {
90 self.score.round().max(0.0) as u32
91 }
92}
93
94#[derive(Debug, Clone, PartialEq)]
96#[non_exhaustive]
97pub enum Answer {
98 Noul(NoulAnswer),
100 Choice(ChoiceAnswer),
102 Score(ScoreAnswer),
104}
105
106impl Answer {
107 pub fn kind(&self) -> &'static str {
109 match self {
110 Answer::Noul(_) => "noul",
111 Answer::Choice(_) => "choice",
112 Answer::Score(_) => "score",
113 }
114 }
115
116 pub fn confidence(&self) -> Option<f64> {
118 match self {
119 Answer::Noul(_) => None,
120 Answer::Choice(a) => Some(a.confidence),
121 Answer::Score(a) => Some(a.confidence),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
128#[non_exhaustive]
129pub struct Usage {
130 #[serde(default)]
132 pub input_tokens: Option<u64>,
133 #[serde(default)]
135 pub output_tokens: Option<u64>,
136}
137
138#[derive(Debug, Clone)]
140#[non_exhaustive]
141pub struct ResponseMeta {
142 pub status: u16,
144 pub headers: HeaderMap,
146 pub attempts: u32,
148}
149
150impl ResponseMeta {
151 pub fn request_id(&self) -> Option<&str> {
153 self.headers
154 .get(REQUEST_ID_HEADER)
155 .and_then(|v| v.to_str().ok())
156 }
157}
158
159#[derive(Debug, Clone)]
161#[non_exhaustive]
162pub struct SystemOneResponse {
163 pub model: String,
165 pub usage: Usage,
167 pub answers: IndexMap<String, Answer>,
170 pub raw: Value,
173 pub meta: ResponseMeta,
175}
176
177impl SystemOneResponse {
178 pub fn request_id(&self) -> Option<&str> {
180 self.meta.request_id()
181 }
182
183 pub fn noul(&self, name: &str) -> Option<&NoulAnswer> {
185 match self.answers.get(name)? {
186 Answer::Noul(a) => Some(a),
187 _ => None,
188 }
189 }
190
191 pub fn choice(&self, name: &str) -> Option<&ChoiceAnswer> {
193 match self.answers.get(name)? {
194 Answer::Choice(a) => Some(a),
195 _ => None,
196 }
197 }
198
199 pub fn score(&self, name: &str) -> Option<&ScoreAnswer> {
201 match self.answers.get(name)? {
202 Answer::Score(a) => Some(a),
203 _ => None,
204 }
205 }
206
207 pub fn nouls(&self) -> impl Iterator<Item = (&str, &NoulAnswer)> {
209 self.answers.iter().filter_map(|(k, a)| match a {
210 Answer::Noul(n) => Some((k.as_str(), n)),
211 _ => None,
212 })
213 }
214
215 pub fn choices(&self) -> impl Iterator<Item = (&str, &ChoiceAnswer)> {
217 self.answers.iter().filter_map(|(k, a)| match a {
218 Answer::Choice(c) => Some((k.as_str(), c)),
219 _ => None,
220 })
221 }
222
223 pub fn scores(&self) -> impl Iterator<Item = (&str, &ScoreAnswer)> {
225 self.answers.iter().filter_map(|(k, a)| match a {
226 Answer::Score(s) => Some((k.as_str(), s)),
227 _ => None,
228 })
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
234#[non_exhaustive]
235pub struct ModelMetadata {
236 pub name: String,
238 pub description: String,
240 pub release_date: String,
242}
243
244#[derive(Debug, Clone)]
246#[non_exhaustive]
247pub struct ListModelsResponse {
248 pub models: Vec<ModelMetadata>,
250 pub raw: Value,
252 pub meta: ResponseMeta,
254}
255
256pub(crate) struct DecodeFailure {
260 pub path: String,
261 pub detail: String,
262}
263
264fn typed<T: DeserializeOwned>(prefix: &str, json: &[u8]) -> Result<T, DecodeFailure> {
269 let mut de = serde_json::Deserializer::from_slice(json);
270 let value = serde_path_to_error::deserialize(&mut de).map_err(|e| {
271 let inner = e.path().to_string();
272 let path = match (prefix.is_empty(), inner.as_str()) {
273 (true, ".") => String::new(),
274 (true, _) => inner.clone(),
275 (false, ".") => prefix.to_owned(),
276 (false, _) => format!("{prefix}.{inner}"),
277 };
278 let msg = e.inner().to_string();
280 let path = match msg
281 .strip_prefix("missing field `")
282 .and_then(|r| r.split('`').next())
283 {
284 Some(field) if path.is_empty() => field.to_owned(),
285 Some(field) => format!("{path}.{field}"),
286 None => path,
287 };
288 DecodeFailure { path, detail: msg }
289 })?;
290 de.end().map_err(|e| DecodeFailure {
291 path: prefix.to_owned(),
292 detail: e.to_string(),
293 })?;
294 Ok(value)
295}
296
297#[derive(Deserialize)]
298struct Envelope {
299 model: String,
300 #[serde(default)]
301 usage: Usage,
302 answers: IndexMap<String, Box<RawValue>>,
303}
304
305pub(crate) struct DecodedSystemOne {
307 pub model: String,
308 pub usage: Usage,
309 pub answers: IndexMap<String, Answer>,
310 pub raw: Value,
311}
312
313pub(crate) fn decode_system_one(body: &[u8]) -> Result<DecodedSystemOne, DecodeFailure> {
314 let env: Envelope = typed("", body)?;
315 let mut answers = IndexMap::with_capacity(env.answers.len());
316 for (name, value) in env.answers {
317 let prefix = format!("answers.{name}");
318 let json = value.get();
319 let tag = serde_json::from_str::<Value>(json)
320 .ok()
321 .and_then(|v| v.get("type").and_then(Value::as_str).map(str::to_owned));
322 let Some(tag) = tag else {
323 return Err(DecodeFailure {
324 path: format!("{prefix}.type"),
325 detail: "missing or non-string answer type".into(),
326 });
327 };
328 let answer = match tag.as_str() {
329 "noul" => Answer::Noul(typed(&prefix, json.as_bytes())?),
330 "choice" => Answer::Choice(typed(&prefix, json.as_bytes())?),
331 "score" => Answer::Score(typed(&prefix, json.as_bytes())?),
332 other => {
333 tracing::warn!(answer = %name, r#type = %other, "ignoring answer with unrecognized type");
334 continue;
335 }
336 };
337 answers.insert(name, answer);
338 }
339 let raw = serde_json::from_slice(body).unwrap_or(Value::Null);
341 Ok(DecodedSystemOne {
342 model: env.model,
343 usage: env.usage,
344 answers,
345 raw,
346 })
347}
348
349#[derive(Deserialize)]
350struct ModelList {
351 models: Vec<ModelMetadata>,
352}
353
354pub(crate) fn decode_models(body: &[u8]) -> Result<(Vec<ModelMetadata>, Value), DecodeFailure> {
355 let list: ModelList = typed("", body)?;
356 let raw = serde_json::from_slice(body).unwrap_or(Value::Null);
357 Ok((list.models, raw))
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use serde_json::json;
364
365 fn sample() -> Value {
366 json!({
367 "model": "jev-latest",
368 "answers": {
369 "department": {"type": "choice", "choice": "technical",
370 "probabilities": {"billing": 0.159, "technical": 0.84, "sales": 0.001}, "confidence": 0.596},
371 "frustration": {"type": "score", "score": 1.6,
372 "legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
373 "probabilities": {"0": 0.05, "1": 0.3, "2": 0.65}, "confidence": 0.78},
374 "is_urgent": {"type": "noul", "noul": 0.999},
375 "future": {"type": "span", "start": 3}
376 },
377 "usage": {"input_tokens": 312, "output_tokens": 48, "extra": true}
378 })
379 }
380
381 fn decode(v: Value) -> Result<DecodedSystemOne, DecodeFailure> {
382 decode_system_one(&serde_json::to_vec(&v).unwrap())
383 }
384
385 #[test]
386 fn decodes_all_types_and_skips_unknown() {
387 let DecodedSystemOne {
388 model,
389 usage,
390 answers,
391 raw,
392 } = decode(sample()).ok().unwrap();
393 assert_eq!(model, "jev-latest");
394 assert_eq!(usage.input_tokens, Some(312));
395 assert_eq!(answers.len(), 3);
396 assert_eq!(raw["answers"]["future"]["start"], json!(3));
397 let Answer::Score(s) = &answers["frustration"] else {
398 panic!()
399 };
400 assert_eq!(s.legend[&2], json!("Very angry"));
401 assert_eq!(s.most_likely_level(), Some(2));
402 assert_eq!(s.rounded_level(), 2);
403 let Answer::Choice(c) = &answers["department"] else {
404 panic!()
405 };
406 assert_eq!(c.ranked()[0], ("technical", 0.84));
407 }
408
409 #[test]
410 fn preserves_server_order_of_probabilities() {
411 let body = br#"{"model":"m","usage":{},"answers":{"c":{"type":"choice","choice":"z",
412 "probabilities":{"z":0.5,"a":0.3,"m":0.2},"confidence":0.1}}}"#;
413 let d = decode_system_one(body).ok().unwrap();
414 let Answer::Choice(c) = &d.answers["c"] else {
415 panic!()
416 };
417 let keys: Vec<_> = c.probabilities.keys().map(String::as_str).collect();
418 assert_eq!(keys, ["z", "a", "m"]);
419 }
420
421 #[test]
422 fn usage_may_be_empty_or_absent() {
423 let d = decode(json!({"model": "m", "answers": {}, "usage": {}}))
424 .ok()
425 .unwrap();
426 assert_eq!(d.usage, Usage::default());
427 let d = decode(json!({"model": "m", "answers": {}})).ok().unwrap();
428 assert_eq!(d.usage, Usage::default());
429 }
430
431 #[test]
432 fn rejects_non_json_and_trailing_content() {
433 assert_eq!(decode_system_one(b"<html>").err().unwrap().path, "");
434 let mut body = serde_json::to_vec(&sample()).unwrap();
435 body.extend_from_slice(b" trailing");
436 assert!(decode_system_one(&body).is_err());
437 }
438
439 #[test]
440 fn reports_precise_paths() {
441 let mut v = sample();
442 v["answers"]["department"]
443 .as_object_mut()
444 .unwrap()
445 .remove("confidence");
446 assert_eq!(
447 decode(v).err().unwrap().path,
448 "answers.department.confidence"
449 );
450
451 let mut v = sample();
452 v["answers"]["frustration"]["probabilities"]["1"] = json!("high");
453 assert_eq!(
454 decode(v).err().unwrap().path,
455 "answers.frustration.probabilities.1"
456 );
457
458 let mut v = sample();
459 v["answers"]["is_urgent"]["type"] = json!(7);
460 assert_eq!(decode(v).err().unwrap().path, "answers.is_urgent.type");
461
462 let mut v = sample();
463 v.as_object_mut().unwrap().remove("model");
464 assert_eq!(decode(v).err().unwrap().path, "model");
465 }
466}