1use std::collections::HashMap;
2
3use serde::Deserialize;
4
5use crate::error::{Error, Result};
6
7#[derive(Clone, Debug, PartialEq, Deserialize)]
9pub struct NoulAnswer {
10 pub noul: f64,
12}
13
14#[derive(Clone, Debug, PartialEq, Deserialize)]
16pub struct ChoiceAnswer {
17 pub choice: String,
19 pub probabilities: HashMap<String, f64>,
21 pub confidence: f64,
23}
24
25impl ChoiceAnswer {
26 #[must_use]
28 pub fn ranked(&self) -> Vec<(&str, f64)> {
29 let mut ranked: Vec<(&str, f64)> = self.probabilities.iter().map(|(k, v)| (k.as_str(), *v)).collect();
30 ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
31 ranked
32 }
33}
34
35#[derive(Clone, Debug, PartialEq, Deserialize)]
37pub struct ScoreAnswer {
38 pub score: f64,
40 pub legend: HashMap<String, String>,
42 pub probabilities: HashMap<String, f64>,
44 pub confidence: f64,
46}
47
48#[derive(Clone, Debug, PartialEq, Deserialize)]
50#[serde(tag = "type", rename_all = "lowercase")]
51#[non_exhaustive]
52pub enum Answer {
53 Noul(NoulAnswer),
55 Choice(ChoiceAnswer),
57 Score(ScoreAnswer),
59}
60
61impl Answer {
62 #[must_use]
64 pub const fn as_noul(&self) -> Option<f64> {
65 match self {
66 Self::Noul(a) => Some(a.noul),
67 _ => None,
68 }
69 }
70
71 #[must_use]
73 pub const fn as_choice(&self) -> Option<&ChoiceAnswer> {
74 match self {
75 Self::Choice(a) => Some(a),
76 _ => None,
77 }
78 }
79
80 #[must_use]
82 pub const fn as_score(&self) -> Option<&ScoreAnswer> {
83 match self {
84 Self::Score(a) => Some(a),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
92pub struct Usage {
93 pub input_tokens: u64,
95 pub output_tokens: u64,
97}
98
99#[derive(Clone, Debug, PartialEq, Deserialize)]
101pub struct SystemOneResponse {
102 pub model: String,
104 pub answers: HashMap<String, Answer>,
106 pub usage: Usage,
108}
109
110impl SystemOneResponse {
111 pub fn noul(&self, id: &str) -> Result<f64> {
117 self.answers
118 .get(id)
119 .and_then(Answer::as_noul)
120 .ok_or_else(|| missing(id, "noul"))
121 }
122
123 pub fn choice(&self, id: &str) -> Result<&ChoiceAnswer> {
129 self.answers
130 .get(id)
131 .and_then(Answer::as_choice)
132 .ok_or_else(|| missing(id, "choice"))
133 }
134
135 pub fn score(&self, id: &str) -> Result<&ScoreAnswer> {
141 self.answers
142 .get(id)
143 .and_then(Answer::as_score)
144 .ok_or_else(|| missing(id, "score"))
145 }
146}
147
148fn missing(id: &str, expected: &'static str) -> Error {
149 Error::MissingAnswer {
150 id: id.to_string(),
151 expected,
152 }
153}
154
155#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
157pub struct ModelInfo {
158 pub name: String,
160 pub description: String,
162 pub release_date: String,
164}
165
166#[derive(Deserialize)]
167pub(crate) struct ModelsResponse {
168 pub(crate) models: Vec<ModelInfo>,
169}
170
171#[cfg(test)]
172#[allow(
173 clippy::float_cmp,
174 reason = "values are parsed verbatim from JSON literals, exact comparison is intended"
175)]
176mod tests {
177 use super::*;
178 use serde_json::json;
179
180 #[test]
181 fn parses_all_three_answer_types() {
182 let body = json!({
183 "model": "jev-1.13.0",
184 "answers": {
185 "is_urgent": {"type": "noul", "noul": 0.92},
186 "department": {"type": "choice", "choice": "technical",
187 "probabilities": {"billing": 0.08, "technical": 0.85, "sales": 0.07}, "confidence": 0.82},
188 "frustration": {"type": "score", "score": 1.6,
189 "legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
190 "probabilities": {"0": 0.05, "1": 0.3, "2": 0.65}, "confidence": 0.78}
191 },
192 "usage": {"input_tokens": 312, "output_tokens": 48}
193 });
194 let resp: SystemOneResponse = serde_json::from_value(body).unwrap();
195 assert_eq!(resp.answers["is_urgent"].as_noul(), Some(0.92));
196 let dept = resp.answers["department"].as_choice().unwrap();
197 assert_eq!(dept.choice, "technical");
198 assert_eq!(dept.ranked()[0], ("technical", 0.85));
199 let score = resp.answers["frustration"].as_score().unwrap();
200 assert_eq!(score.legend["2"], "Very angry");
201 assert_eq!(resp.usage.input_tokens, 312);
202 assert!(resp.answers["is_urgent"].as_choice().is_none());
203 assert_eq!(resp.noul("is_urgent").unwrap(), 0.92);
204 assert_eq!(resp.choice("department").unwrap().choice, "technical");
205 assert!(matches!(
206 resp.choice("is_urgent"),
207 Err(Error::MissingAnswer { expected: "choice", .. })
208 ));
209 assert!(matches!(resp.score("nope"), Err(Error::MissingAnswer { .. })));
210 }
211}