1use std::collections::BTreeMap;
8use std::fmt;
9use std::str::FromStr;
10
11use http::StatusCode;
12use http::header::HeaderMap;
13use indexmap::IndexMap;
14use serde::de::{self, DeserializeOwned, Deserializer};
15use serde::ser::{SerializeMap, SerializeStruct, Serializer};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use serde_json::value::RawValue;
19
20use crate::constants::REQUEST_ID_HEADER;
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[non_exhaustive]
25pub struct NoulAnswer {
26 pub noul: f64,
28}
29
30impl NoulAnswer {
31 pub fn is_yes(&self, threshold: f64) -> bool {
33 self.noul >= threshold
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[non_exhaustive]
40pub struct ChoiceAnswer {
41 pub choice: String,
43 pub probabilities: IndexMap<String, f64>,
45 pub confidence: f64,
47}
48
49impl ChoiceAnswer {
50 pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
56 self.choice.parse()
57 }
58
59 pub fn probability(&self, label: &str) -> Option<f64> {
61 self.probabilities.get(label).copied()
62 }
63
64 pub fn ranked(&self) -> Vec<(&str, f64)> {
66 let mut v: Vec<_> = self
67 .probabilities
68 .iter()
69 .map(|(k, p)| (k.as_str(), *p))
70 .collect();
71 v.sort_by(|a, b| b.1.total_cmp(&a.1));
72 v
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[non_exhaustive]
79pub struct ScoreAnswer {
80 pub score: f64,
82 pub confidence: f64,
84 pub legend: BTreeMap<u32, Value>,
86 pub probabilities: BTreeMap<u32, f64>,
88}
89
90impl ScoreAnswer {
91 pub fn most_likely_level(&self) -> Option<u32> {
93 self.probabilities
94 .iter()
95 .max_by(|a, b| a.1.total_cmp(b.1))
96 .map(|(level, _)| *level)
97 }
98
99 pub fn rounded_level(&self) -> u32 {
101 self.score.round().max(0.0) as u32
102 }
103}
104
105#[derive(Debug, Clone, PartialEq)]
107#[non_exhaustive]
108pub enum Answer {
109 Noul(NoulAnswer),
111 Choice(ChoiceAnswer),
113 Score(ScoreAnswer),
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[non_exhaustive]
120pub enum AnswerKind {
121 Noul,
123 Choice,
125 Score,
127}
128
129impl AnswerKind {
130 pub const fn as_str(self) -> &'static str {
132 match self {
133 AnswerKind::Noul => "noul",
134 AnswerKind::Choice => "choice",
135 AnswerKind::Score => "score",
136 }
137 }
138}
139
140impl fmt::Display for AnswerKind {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 f.write_str(self.as_str())
143 }
144}
145
146impl Answer {
147 pub fn kind(&self) -> AnswerKind {
149 match self {
150 Answer::Noul(_) => AnswerKind::Noul,
151 Answer::Choice(_) => AnswerKind::Choice,
152 Answer::Score(_) => AnswerKind::Score,
153 }
154 }
155
156 pub fn confidence(&self) -> Option<f64> {
158 match self {
159 Answer::Noul(_) => None,
160 Answer::Choice(a) => Some(a.confidence),
161 Answer::Score(a) => Some(a.confidence),
162 }
163 }
164}
165
166impl Serialize for Answer {
168 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
169 let mut m = s.serialize_map(None)?;
170 m.serialize_entry("type", self.kind().as_str())?;
171 match self {
172 Answer::Noul(a) => m.serialize_entry("noul", &a.noul)?,
173 Answer::Choice(a) => {
174 m.serialize_entry("choice", &a.choice)?;
175 m.serialize_entry("probabilities", &a.probabilities)?;
176 m.serialize_entry("confidence", &a.confidence)?;
177 }
178 Answer::Score(a) => {
179 m.serialize_entry("score", &a.score)?;
180 m.serialize_entry("legend", &a.legend)?;
181 m.serialize_entry("probabilities", &a.probabilities)?;
182 m.serialize_entry("confidence", &a.confidence)?;
183 }
184 }
185 m.end()
186 }
187}
188
189impl<'de> Deserialize<'de> for Answer {
191 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
192 #[derive(Deserialize)]
196 struct Wire {
197 #[serde(rename = "type")]
198 kind: String,
199 noul: Option<f64>,
200 choice: Option<String>,
201 score: Option<f64>,
202 confidence: Option<f64>,
203 legend: Option<BTreeMap<u32, Value>>,
204 probabilities: Option<IndexMap<String, f64>>,
205 }
206
207 fn need<T, E: de::Error>(v: Option<T>, field: &'static str) -> Result<T, E> {
208 v.ok_or_else(|| E::missing_field(field))
209 }
210
211 let w = Wire::deserialize(d)?;
212 match w.kind.as_str() {
213 "noul" => Ok(Answer::Noul(NoulAnswer {
214 noul: need(w.noul, "noul")?,
215 })),
216 "choice" => Ok(Answer::Choice(ChoiceAnswer {
217 choice: need(w.choice, "choice")?,
218 probabilities: need(w.probabilities, "probabilities")?,
219 confidence: need(w.confidence, "confidence")?,
220 })),
221 "score" => Ok(Answer::Score(ScoreAnswer {
222 score: need(w.score, "score")?,
223 confidence: need(w.confidence, "confidence")?,
224 legend: need(w.legend, "legend")?,
225 probabilities: need(w.probabilities, "probabilities")?
226 .into_iter()
227 .map(|(k, p)| match k.parse() {
228 Ok(level) => Ok((level, p)),
229 Err(_) => Err(de::Error::custom(format!("invalid level {k:?}"))),
230 })
231 .collect::<Result<_, D::Error>>()?,
232 })),
233 other => Err(de::Error::unknown_variant(
234 other,
235 &["noul", "choice", "score"],
236 )),
237 }
238 }
239}
240
241#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
243#[non_exhaustive]
244pub struct Usage {
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub input_tokens: Option<u64>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub output_tokens: Option<u64>,
251}
252
253#[derive(Debug, Clone)]
255#[non_exhaustive]
256pub struct ResponseMeta {
257 pub status: StatusCode,
259 pub headers: HeaderMap,
261 pub attempts: u32,
263}
264
265impl ResponseMeta {
266 pub fn request_id(&self) -> Option<&str> {
268 self.headers
269 .get(REQUEST_ID_HEADER)
270 .and_then(|v| v.to_str().ok())
271 }
272}
273
274#[derive(Debug, Clone)]
276#[non_exhaustive]
277pub struct SystemOneResponse {
278 pub model: String,
280 pub usage: Usage,
282 pub answers: IndexMap<String, Answer>,
285 pub raw: Value,
288 pub meta: ResponseMeta,
290}
291
292impl SystemOneResponse {
293 pub fn request_id(&self) -> Option<&str> {
295 self.meta.request_id()
296 }
297
298 pub fn noul(&self, name: &str) -> Option<&NoulAnswer> {
300 match self.answers.get(name)? {
301 Answer::Noul(a) => Some(a),
302 _ => None,
303 }
304 }
305
306 pub fn choice(&self, name: &str) -> Option<&ChoiceAnswer> {
308 match self.answers.get(name)? {
309 Answer::Choice(a) => Some(a),
310 _ => None,
311 }
312 }
313
314 pub fn score(&self, name: &str) -> Option<&ScoreAnswer> {
316 match self.answers.get(name)? {
317 Answer::Score(a) => Some(a),
318 _ => None,
319 }
320 }
321
322 pub fn nouls(&self) -> impl Iterator<Item = (&str, &NoulAnswer)> {
324 self.answers.iter().filter_map(|(k, a)| match a {
325 Answer::Noul(n) => Some((k.as_str(), n)),
326 _ => None,
327 })
328 }
329
330 pub fn choices(&self) -> impl Iterator<Item = (&str, &ChoiceAnswer)> {
332 self.answers.iter().filter_map(|(k, a)| match a {
333 Answer::Choice(c) => Some((k.as_str(), c)),
334 _ => None,
335 })
336 }
337
338 pub fn scores(&self) -> impl Iterator<Item = (&str, &ScoreAnswer)> {
340 self.answers.iter().filter_map(|(k, a)| match a {
341 Answer::Score(s) => Some((k.as_str(), s)),
342 _ => None,
343 })
344 }
345}
346
347impl Serialize for SystemOneResponse {
351 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
352 let mut st = s.serialize_struct("SystemOneResponse", 3)?;
353 st.serialize_field("model", &self.model)?;
354 st.serialize_field("answers", &self.answers)?;
355 st.serialize_field("usage", &self.usage)?;
356 st.end()
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362#[non_exhaustive]
363pub struct ModelMetadata {
364 pub name: String,
366 pub description: String,
368 pub release_date: String,
370}
371
372impl Serialize for ListModelsResponse {
374 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
375 let mut st = s.serialize_struct("ListModelsResponse", 1)?;
376 st.serialize_field("models", &self.models)?;
377 st.end()
378 }
379}
380
381#[derive(Debug, Clone)]
383#[non_exhaustive]
384pub struct ListModelsResponse {
385 pub models: Vec<ModelMetadata>,
387 pub raw: Value,
389 pub meta: ResponseMeta,
391}
392
393pub(crate) struct DecodeFailure {
397 pub path: String,
398 pub detail: String,
399}
400
401fn typed<T: DeserializeOwned>(prefix: &str, json: &[u8]) -> Result<T, DecodeFailure> {
406 let mut de = serde_json::Deserializer::from_slice(json);
407 let value = serde_path_to_error::deserialize(&mut de).map_err(|e| {
408 let inner = e.path().to_string();
409 let path = match (prefix.is_empty(), inner.as_str()) {
410 (true, ".") => String::new(),
411 (true, _) => inner.clone(),
412 (false, ".") => prefix.to_owned(),
413 (false, _) => format!("{prefix}.{inner}"),
414 };
415 let msg = e.inner().to_string();
417 let path = match msg
418 .strip_prefix("missing field `")
419 .and_then(|r| r.split('`').next())
420 {
421 Some(field) if path.is_empty() => field.to_owned(),
422 Some(field) => format!("{path}.{field}"),
423 None => path,
424 };
425 DecodeFailure { path, detail: msg }
426 })?;
427 de.end().map_err(|e| DecodeFailure {
428 path: prefix.to_owned(),
429 detail: e.to_string(),
430 })?;
431 Ok(value)
432}
433
434#[derive(Deserialize)]
435struct Envelope {
436 model: String,
437 #[serde(default)]
438 usage: Usage,
439 answers: IndexMap<String, Box<RawValue>>,
440}
441
442pub(crate) struct DecodedSystemOne {
444 pub model: String,
445 pub usage: Usage,
446 pub answers: IndexMap<String, Answer>,
447 pub raw: Value,
448}
449
450pub(crate) fn decode_system_one(body: &[u8]) -> Result<DecodedSystemOne, DecodeFailure> {
451 let env: Envelope = typed("", body)?;
452 let mut answers = IndexMap::with_capacity(env.answers.len());
453 for (name, value) in env.answers {
454 let prefix = format!("answers.{name}");
455 let json = value.get();
456 let tag = serde_json::from_str::<Value>(json)
457 .ok()
458 .and_then(|v| v.get("type").and_then(Value::as_str).map(str::to_owned));
459 let Some(tag) = tag else {
460 return Err(DecodeFailure {
461 path: format!("{prefix}.type"),
462 detail: "missing or non-string answer type".into(),
463 });
464 };
465 let answer = match tag.as_str() {
466 "noul" => Answer::Noul(typed(&prefix, json.as_bytes())?),
467 "choice" => Answer::Choice(typed(&prefix, json.as_bytes())?),
468 "score" => Answer::Score(typed(&prefix, json.as_bytes())?),
469 other => {
470 tracing::warn!(answer = %name, r#type = %other, "ignoring answer with unrecognized type");
471 continue;
472 }
473 };
474 answers.insert(name, answer);
475 }
476 let raw = serde_json::from_slice(body).unwrap_or(Value::Null);
478 Ok(DecodedSystemOne {
479 model: env.model,
480 usage: env.usage,
481 answers,
482 raw,
483 })
484}
485
486#[derive(Deserialize)]
487struct ModelList {
488 models: Vec<ModelMetadata>,
489}
490
491pub(crate) fn decode_models(body: &[u8]) -> Result<(Vec<ModelMetadata>, Value), DecodeFailure> {
492 let list: ModelList = typed("", body)?;
493 let raw = serde_json::from_slice(body).unwrap_or(Value::Null);
494 Ok((list.models, raw))
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use serde_json::json;
501
502 fn sample() -> Value {
503 json!({
504 "model": "jev-latest",
505 "answers": {
506 "department": {"type": "choice", "choice": "technical",
507 "probabilities": {"billing": 0.159, "technical": 0.84, "sales": 0.001}, "confidence": 0.596},
508 "frustration": {"type": "score", "score": 1.6,
509 "legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
510 "probabilities": {"0": 0.05, "1": 0.3, "2": 0.65}, "confidence": 0.78},
511 "is_urgent": {"type": "noul", "noul": 0.999},
512 "future": {"type": "span", "start": 3}
513 },
514 "usage": {"input_tokens": 312, "output_tokens": 48, "extra": true}
515 })
516 }
517
518 fn decode(v: Value) -> Result<DecodedSystemOne, DecodeFailure> {
519 decode_system_one(&serde_json::to_vec(&v).unwrap())
520 }
521
522 #[test]
523 fn decodes_all_types_and_skips_unknown() {
524 let DecodedSystemOne {
525 model,
526 usage,
527 answers,
528 raw,
529 } = decode(sample()).ok().unwrap();
530 assert_eq!(model, "jev-latest");
531 assert_eq!(usage.input_tokens, Some(312));
532 assert_eq!(answers.len(), 3);
533 assert_eq!(raw["answers"]["future"]["start"], json!(3));
534 let Answer::Score(s) = &answers["frustration"] else {
535 panic!()
536 };
537 assert_eq!(s.legend[&2], json!("Very angry"));
538 assert_eq!(s.most_likely_level(), Some(2));
539 assert_eq!(s.rounded_level(), 2);
540 let Answer::Choice(c) = &answers["department"] else {
541 panic!()
542 };
543 assert_eq!(c.ranked()[0], ("technical", 0.84));
544 }
545
546 #[test]
547 fn serializes_back_to_the_wire_shape() {
548 let mut wire = sample();
549 wire["answers"].as_object_mut().unwrap().remove("future");
550 wire["usage"].as_object_mut().unwrap().remove("extra");
551 let d = decode(wire.clone()).ok().unwrap();
552 let res = SystemOneResponse {
553 model: d.model,
554 usage: d.usage,
555 answers: d.answers,
556 raw: d.raw,
557 meta: ResponseMeta {
558 status: StatusCode::OK,
559 headers: HeaderMap::new(),
560 attempts: 1,
561 },
562 };
563 let body = serde_json::to_vec(&res).unwrap();
565 assert_eq!(serde_json::from_slice::<Value>(&body).unwrap(), wire);
566 let again = decode_system_one(&body).ok().unwrap();
567 assert_eq!(again.model, res.model);
568 assert_eq!(again.usage, res.usage);
569 assert_eq!(again.answers, res.answers);
570
571 for (name, answer) in &res.answers {
572 assert_eq!(answer.kind().to_string(), wire["answers"][name]["type"]);
573 let text = serde_json::to_string(answer).unwrap();
574 assert_eq!(serde_json::from_str::<Answer>(&text).unwrap(), *answer);
575 assert_eq!(
576 serde_json::from_str::<Value>(&text).unwrap(),
577 wire["answers"][name]
578 );
579 }
580 let text = r#"{"type":"choice","choice":"z","probabilities":{"z":0.5,"a":0.3,"m":0.2},"confidence":0.1}"#;
582 let answer: Answer = serde_json::from_str(text).unwrap();
583 assert_eq!(serde_json::to_string(&answer).unwrap(), text);
584
585 assert_eq!(serde_json::to_value(Usage::default()).unwrap(), json!({}));
586 assert!(serde_json::from_value::<Answer>(json!({"type": "span"})).is_err());
587 assert!(serde_json::from_value::<Answer>(json!({"type": "noul"})).is_err());
588
589 let body =
590 br#"{"models":[{"name":"jev-latest","description":"d","release_date":"2025-01-01"}]}"#;
591 let (models, raw) = decode_models(body).ok().unwrap();
592 let list = ListModelsResponse {
593 models,
594 raw: raw.clone(),
595 meta: res.meta.clone(),
596 };
597 assert_eq!(serde_json::to_value(&list).unwrap(), raw);
598 }
599
600 #[test]
601 fn preserves_server_order_of_probabilities() {
602 let body = br#"{"model":"m","usage":{},"answers":{"c":{"type":"choice","choice":"z",
603 "probabilities":{"z":0.5,"a":0.3,"m":0.2},"confidence":0.1}}}"#;
604 let d = decode_system_one(body).ok().unwrap();
605 let Answer::Choice(c) = &d.answers["c"] else {
606 panic!()
607 };
608 let keys: Vec<_> = c.probabilities.keys().map(String::as_str).collect();
609 assert_eq!(keys, ["z", "a", "m"]);
610 }
611
612 #[test]
613 fn usage_may_be_empty_or_absent() {
614 let d = decode(json!({"model": "m", "answers": {}, "usage": {}}))
615 .ok()
616 .unwrap();
617 assert_eq!(d.usage, Usage::default());
618 let d = decode(json!({"model": "m", "answers": {}})).ok().unwrap();
619 assert_eq!(d.usage, Usage::default());
620 }
621
622 #[test]
623 fn rejects_non_json_and_trailing_content() {
624 assert_eq!(decode_system_one(b"<html>").err().unwrap().path, "");
625 let mut body = serde_json::to_vec(&sample()).unwrap();
626 body.extend_from_slice(b" trailing");
627 assert!(decode_system_one(&body).is_err());
628 }
629
630 #[test]
631 fn reports_precise_paths() {
632 let mut v = sample();
633 v["answers"]["department"]
634 .as_object_mut()
635 .unwrap()
636 .remove("confidence");
637 assert_eq!(
638 decode(v).err().unwrap().path,
639 "answers.department.confidence"
640 );
641
642 let mut v = sample();
643 v["answers"]["frustration"]["probabilities"]["1"] = json!("high");
644 assert_eq!(
645 decode(v).err().unwrap().path,
646 "answers.frustration.probabilities.1"
647 );
648
649 let mut v = sample();
650 v["answers"]["is_urgent"]["type"] = json!(7);
651 assert_eq!(decode(v).err().unwrap().path, "answers.is_urgent.type");
652
653 let mut v = sample();
654 v.as_object_mut().unwrap().remove("model");
655 assert_eq!(decode(v).err().unwrap().path, "model");
656 }
657}