typesafe_jev/answer.rs
1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6use crate::Content;
7
8/// What one request came back with: an [`Answer`] per question, under the id the question had.
9///
10/// ```
11/// use typesafe_jev::{Answer, Response};
12///
13/// let response: Response = serde_json::from_str(r#"{
14/// "model": "jev-1.13.0",
15/// "answers": {
16/// "is_urgent": {"type": "noul", "noul": 0.95},
17/// "department": {"type": "choice", "choice": "billing", "confidence": 0.81,
18/// "probabilities": {"billing": 0.88, "technical": 0.12, "sales": 0.0}}
19/// },
20/// "usage": {"input_tokens": 318, "output_tokens": 34}
21/// }"#)?;
22///
23/// assert_eq!(response.noul("is_urgent").map(|answer| answer.noul), Some(0.95));
24/// let department = response.choice("department").expect("asked as a choice");
25/// assert_eq!((department.choice.as_str(), department.probabilities["technical"]), ("billing", 0.12));
26/// assert!(response.score("department").is_none(), "it is there, but it is not a score");
27/// assert!(matches!(response.answers["is_urgent"], Answer::Noul(_)));
28/// # Ok::<(), serde_json::Error>(())
29/// ```
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[non_exhaustive]
32pub struct Response {
33 /// The model that performed the evaluation: the release that an alias like `jev-latest`
34 /// stood for.
35 pub model: String,
36 /// One answer per question, keyed and ordered as the API returned them.
37 pub answers: IndexMap<String, Answer>,
38 /// The tokens this request used.
39 #[serde(default)]
40 pub usage: TokenUsage,
41}
42
43impl Response {
44 /// The answer under `id`, if there is one and it answers a [`Noul`](crate::Noul).
45 pub fn noul(&self, id: &str) -> Option<&NoulAnswer> {
46 self.answers.get(id).and_then(Answer::as_noul)
47 }
48
49 /// The answer under `id`, if there is one and it answers a [`Choice`](crate::Choice).
50 pub fn choice(&self, id: &str) -> Option<&ChoiceAnswer> {
51 self.answers.get(id).and_then(Answer::as_choice)
52 }
53
54 /// The answer under `id`, if there is one and it answers a [`Score`](crate::Score).
55 pub fn score(&self, id: &str) -> Option<&ScoreAnswer> {
56 self.answers.get(id).and_then(Answer::as_score)
57 }
58}
59
60/// The tokens one request used, as the API reports them. A count the API left out is `None`.
61///
62/// [`Usage`](crate::Usage) is the running total a [`Client`](crate::Client) keeps of these.
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
64#[non_exhaustive]
65pub struct TokenUsage {
66 /// Tokens of state and questions. These are what Jev charges for.
67 #[serde(default)]
68 pub input_tokens: Option<u64>,
69 /// Tokens of answers. Jev reports them, but does not charge for them.
70 #[serde(default)]
71 pub output_tokens: Option<u64>,
72}
73
74/// The answer to one [`Question`](crate::Question), of the type the question had.
75///
76/// A [`ChoiceAnswer`] and a [`ScoreAnswer`] carry a `confidence` next to their probabilities: the
77/// probabilities say which answer, the confidence says how far to trust it. A [`NoulAnswer`] is
78/// a single calibrated probability and needs none.
79///
80/// The enum is `#[non_exhaustive]`, like [`Question`](crate::Question).
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82#[serde(tag = "type", rename_all = "lowercase", try_from = "RawAnswer")]
83#[non_exhaustive]
84pub enum Answer {
85 /// The answer to a [`Noul`](crate::Noul).
86 Noul(NoulAnswer),
87 /// The answer to a [`Choice`](crate::Choice).
88 Choice(ChoiceAnswer),
89 /// The answer to a [`Score`](crate::Score).
90 Score(ScoreAnswer),
91}
92
93impl Answer {
94 /// The answer, if it answers a [`Noul`](crate::Noul).
95 pub fn as_noul(&self) -> Option<&NoulAnswer> {
96 match self {
97 Answer::Noul(answer) => Some(answer),
98 _ => None,
99 }
100 }
101
102 /// The answer, if it answers a [`Choice`](crate::Choice).
103 pub fn as_choice(&self) -> Option<&ChoiceAnswer> {
104 match self {
105 Answer::Choice(answer) => Some(answer),
106 _ => None,
107 }
108 }
109
110 /// The answer, if it answers a [`Score`](crate::Score).
111 pub fn as_score(&self) -> Option<&ScoreAnswer> {
112 match self {
113 Answer::Score(answer) => Some(answer),
114 _ => None,
115 }
116 }
117}
118
119/// The answer to a yes/no question.
120#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
121#[non_exhaustive]
122pub struct NoulAnswer {
123 /// The probability that the answer is yes, from 0 (no) to 1 (yes). It is calibrated: of the
124 /// answers given as 0.9, about nine in ten are a yes. Near 0.5 the model does not know.
125 pub noul: f64,
126}
127
128impl NoulAnswer {
129 /// An answer with this probability of a yes, for tests and fakes.
130 pub fn new(noul: f64) -> Self {
131 NoulAnswer { noul }
132 }
133}
134
135/// The answer to a [`Choice`](crate::Choice): the selected option and the probability of each.
136#[derive(Debug, Clone, PartialEq, Serialize)]
137#[non_exhaustive]
138pub struct ChoiceAnswer {
139 /// The option with the highest probability.
140 pub choice: String,
141 /// How certain the model is of the selection, from 0 to 1, derived from `probabilities`.
142 pub confidence: f64,
143 /// Every option of the question and its probability. They sum to about 1.
144 pub probabilities: IndexMap<String, f64>,
145}
146
147impl ChoiceAnswer {
148 /// An answer, for tests and fakes.
149 pub fn new(choice: impl Into<String>, confidence: f64, probabilities: IndexMap<String, f64>) -> Self {
150 ChoiceAnswer { choice: choice.into(), confidence, probabilities }
151 }
152}
153
154/// The answer to a [`Score`](crate::Score): a position on the question's scale.
155///
156/// Levels are numbered from 0 in the order of the question's `criteria`. On the wire the keys of
157/// `legend` and `probabilities` are those numbers as strings; here they are numbers, in order.
158#[derive(Debug, Clone, PartialEq, Serialize)]
159#[non_exhaustive]
160pub struct ScoreAnswer {
161 /// The probability-weighted average of the levels. It can fall between two levels.
162 pub score: f64,
163 /// How certain the model is of the score, from 0 to 1, derived from `probabilities`.
164 pub confidence: f64,
165 /// Each level and the description the question gave it.
166 pub legend: BTreeMap<u32, Content>,
167 /// Each level and its probability. They sum to about 1.
168 pub probabilities: BTreeMap<u32, f64>,
169}
170
171impl ScoreAnswer {
172 /// An answer, for tests and fakes.
173 pub fn new(score: f64, confidence: f64, legend: BTreeMap<u32, Content>, probabilities: BTreeMap<u32, f64>) -> Self {
174 ScoreAnswer { score, confidence, legend, probabilities }
175 }
176}
177
178/// An answer as it is on the wire, before its `type` says which fields it must have.
179///
180/// `Answer` is read through this rather than through serde's own internally tagged enums: those
181/// buffer the object to find the tag, which garbles `f64` fields when a dependent crate turns
182/// on `serde_json`'s `arbitrary_precision`, and their errors do not say which field is missing
183/// from which type of answer.
184#[derive(Deserialize)]
185struct RawAnswer {
186 r#type: String,
187 noul: Option<f64>,
188 choice: Option<String>,
189 score: Option<f64>,
190 confidence: Option<f64>,
191 probabilities: Option<IndexMap<String, f64>>,
192 legend: Option<IndexMap<String, Content>>,
193}
194
195impl TryFrom<RawAnswer> for Answer {
196 type Error = String;
197
198 fn try_from(raw: RawAnswer) -> Result<Self, String> {
199 fn required<T>(field: Option<T>, name: &str, kind: &str) -> Result<T, String> {
200 field.ok_or_else(|| format!("a `{kind}` answer has no `{name}`"))
201 }
202 fn by_level<T>(map: IndexMap<String, T>, name: &str) -> Result<BTreeMap<u32, T>, String> {
203 map.into_iter()
204 .map(|(level, value)| match level.parse() {
205 Ok(level) => Ok((level, value)),
206 Err(_) => Err(format!("a `score` answer's `{name}` has the key `{level}`, which is not a level number")),
207 })
208 .collect()
209 }
210 let kind = raw.r#type.as_str();
211 match kind {
212 "noul" => Ok(Answer::Noul(NoulAnswer { noul: required(raw.noul, "noul", kind)? })),
213 "choice" => Ok(Answer::Choice(ChoiceAnswer {
214 choice: required(raw.choice, "choice", kind)?,
215 confidence: required(raw.confidence, "confidence", kind)?,
216 probabilities: required(raw.probabilities, "probabilities", kind)?,
217 })),
218 "score" => Ok(Answer::Score(ScoreAnswer {
219 score: required(raw.score, "score", kind)?,
220 confidence: required(raw.confidence, "confidence", kind)?,
221 legend: by_level(required(raw.legend, "legend", kind)?, "legend")?,
222 probabilities: by_level(required(raw.probabilities, "probabilities", kind)?, "probabilities")?,
223 })),
224 other => Err(format!("unknown answer type `{other}`; this client reads `noul`, `choice` and `score`")),
225 }
226 }
227}