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 pub fn ranked(&self) -> Vec<(&str, f64)> {
28 let mut ranked: Vec<(&str, f64)> = self.probabilities.iter().map(|(k, v)| (k.as_str(), *v)).collect();
29 ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
30 ranked
31 }
32}
33
34#[derive(Clone, Debug, PartialEq, Deserialize)]
36pub struct ScoreAnswer {
37 pub score: f64,
39 pub legend: HashMap<String, String>,
41 pub probabilities: HashMap<String, f64>,
43 pub confidence: f64,
45}
46
47#[derive(Clone, Debug, PartialEq, Deserialize)]
49#[serde(tag = "type", rename_all = "lowercase")]
50#[non_exhaustive]
51pub enum Answer {
52 Noul(NoulAnswer),
53 Choice(ChoiceAnswer),
54 Score(ScoreAnswer),
55}
56
57impl Answer {
58 pub fn as_noul(&self) -> Option<f64> {
60 match self {
61 Self::Noul(a) => Some(a.noul),
62 _ => None,
63 }
64 }
65
66 pub fn as_choice(&self) -> Option<&ChoiceAnswer> {
68 match self {
69 Self::Choice(a) => Some(a),
70 _ => None,
71 }
72 }
73
74 pub fn as_score(&self) -> Option<&ScoreAnswer> {
76 match self {
77 Self::Score(a) => Some(a),
78 _ => None,
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
85pub struct Usage {
86 pub input_tokens: u64,
87 pub output_tokens: u64,
88}
89
90#[derive(Clone, Debug, PartialEq, Deserialize)]
92pub struct SystemOneResponse {
93 pub model: String,
95 pub answers: HashMap<String, Answer>,
97 pub usage: Usage,
98}
99
100impl SystemOneResponse {
101 pub fn noul(&self, id: &str) -> Result<f64> {
103 self.answers
104 .get(id)
105 .and_then(Answer::as_noul)
106 .ok_or_else(|| missing(id, "noul"))
107 }
108
109 pub fn choice(&self, id: &str) -> Result<&ChoiceAnswer> {
111 self.answers
112 .get(id)
113 .and_then(Answer::as_choice)
114 .ok_or_else(|| missing(id, "choice"))
115 }
116
117 pub fn score(&self, id: &str) -> Result<&ScoreAnswer> {
119 self.answers
120 .get(id)
121 .and_then(Answer::as_score)
122 .ok_or_else(|| missing(id, "score"))
123 }
124}
125
126fn missing(id: &str, expected: &'static str) -> Error {
127 Error::MissingAnswer {
128 id: id.to_string(),
129 expected,
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
135pub struct ModelInfo {
136 pub name: String,
138 pub description: String,
139 pub release_date: String,
140}
141
142#[derive(Deserialize)]
143pub(crate) struct ModelsResponse {
144 pub(crate) models: Vec<ModelInfo>,
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use serde_json::json;
151
152 #[test]
153 fn parses_all_three_answer_types() {
154 let body = json!({
155 "model": "jev-1.13.0",
156 "answers": {
157 "is_urgent": {"type": "noul", "noul": 0.92},
158 "department": {"type": "choice", "choice": "technical",
159 "probabilities": {"billing": 0.08, "technical": 0.85, "sales": 0.07}, "confidence": 0.82},
160 "frustration": {"type": "score", "score": 1.6,
161 "legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
162 "probabilities": {"0": 0.05, "1": 0.3, "2": 0.65}, "confidence": 0.78}
163 },
164 "usage": {"input_tokens": 312, "output_tokens": 48}
165 });
166 let resp: SystemOneResponse = serde_json::from_value(body).unwrap();
167 assert_eq!(resp.answers["is_urgent"].as_noul(), Some(0.92));
168 let dept = resp.answers["department"].as_choice().unwrap();
169 assert_eq!(dept.choice, "technical");
170 assert_eq!(dept.ranked()[0], ("technical", 0.85));
171 let score = resp.answers["frustration"].as_score().unwrap();
172 assert_eq!(score.legend["2"], "Very angry");
173 assert_eq!(resp.usage.input_tokens, 312);
174 assert!(resp.answers["is_urgent"].as_choice().is_none());
175 assert_eq!(resp.noul("is_urgent").unwrap(), 0.92);
176 assert_eq!(resp.choice("department").unwrap().choice, "technical");
177 assert!(matches!(
178 resp.choice("is_urgent"),
179 Err(Error::MissingAnswer { expected: "choice", .. })
180 ));
181 assert!(matches!(resp.score("nope"), Err(Error::MissingAnswer { .. })));
182 }
183}