Skip to main content

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}