Skip to main content

cerno_sdk/
response.rs

1//! Reading answers back out.
2
3use crate::Error;
4use cerno_types::{Answer, SystemOneResponse, Usage};
5
6/// The answers to one request, with accessors that fail loudly on the wrong id or type.
7///
8/// Reaching for `noul("team")` when `team` was a choice is a programming mistake, not a runtime
9/// condition, and the error says which id and which types rather than handing back a default.
10#[derive(Debug, Clone)]
11pub struct Answers {
12    response: SystemOneResponse,
13}
14
15fn name_of(answer: &Answer) -> &'static str {
16    match answer {
17        Answer::Noul { .. } => "noul",
18        Answer::Choice { .. } => "choice",
19        Answer::Score { .. } => "score",
20    }
21}
22
23/// Wrap a response that was obtained some other way — read from a file, replayed from a log, or
24/// parsed by a caller doing its own transport. The accessors are the useful part of this type,
25/// and they should not be reachable only by going over the network.
26impl From<SystemOneResponse> for Answers {
27    fn from(response: SystemOneResponse) -> Self {
28        Self { response }
29    }
30}
31
32impl Answers {
33    pub(crate) fn new(response: SystemOneResponse) -> Self {
34        Self { response }
35    }
36
37    /// The model that answered, after alias resolution.
38    pub fn model(&self) -> &str {
39        &self.response.model
40    }
41
42    pub fn usage(&self) -> Usage {
43        self.response.usage
44    }
45
46    pub fn timing_ms(&self) -> u64 {
47        self.response.timing_ms.total
48    }
49
50    /// The raw answer for `id`.
51    pub fn get(&self, id: &str) -> Result<&Answer, Error> {
52        self.response
53            .answers
54            .get(id)
55            .ok_or_else(|| Error::MissingAnswer(id.to_string()))
56    }
57
58    /// Probability that the answer to `id` is yes.
59    pub fn noul(&self, id: &str) -> Result<f64, Error> {
60        match self.get(id)? {
61            Answer::Noul { noul, .. } => Ok(*noul),
62            other => Err(self.wrong_type(id, "noul", other)),
63        }
64    }
65
66    /// The winning option for `id`.
67    pub fn choice(&self, id: &str) -> Result<&str, Error> {
68        match self.get(id)? {
69            Answer::Choice { choice, .. } => Ok(choice),
70            other => Err(self.wrong_type(id, "choice", other)),
71        }
72    }
73
74    /// The winning option's position in the request's options, 0-based.
75    pub fn index(&self, id: &str) -> Result<usize, Error> {
76        match self.get(id)? {
77            Answer::Choice { index, .. } => Ok(*index),
78            other => Err(self.wrong_type(id, "choice", other)),
79        }
80    }
81
82    /// The winning level for `id`, 1-based.
83    pub fn score(&self, id: &str) -> Result<u8, Error> {
84        match self.get(id)? {
85            Answer::Score { score, .. } => Ok(*score),
86            other => Err(self.wrong_type(id, "score", other)),
87        }
88    }
89
90    /// The probability-weighted mean level for `id`.
91    pub fn expected_score(&self, id: &str) -> Result<f64, Error> {
92        match self.get(id)? {
93            Answer::Score { expected_score, .. } => Ok(*expected_score),
94            other => Err(self.wrong_type(id, "score", other)),
95        }
96    }
97
98    /// The legend entry the winning level carries.
99    pub fn legend(&self, id: &str) -> Result<&str, Error> {
100        match self.get(id)? {
101            Answer::Score { legend, .. } => Ok(legend),
102            other => Err(self.wrong_type(id, "score", other)),
103        }
104    }
105
106    /// How peaked the distribution behind `id` was, in `0.0..=1.0`. A noul has none, as in
107    /// JEV: its probability is already the whole answer.
108    pub fn confidence(&self, id: &str) -> Result<f64, Error> {
109        let answer = self.get(id)?;
110        answer
111            .confidence()
112            .ok_or_else(|| self.wrong_type(id, "choice or score", answer))
113    }
114
115    /// Whether some label for `id` fell outside the host's reporting window, which makes its
116    /// probability an upper bound rather than an observation.
117    pub fn truncated(&self, id: &str) -> Result<bool, Error> {
118        Ok(self.get(id)?.truncated())
119    }
120
121    /// How much of the model's first-token probability fell on the offered labels for `id`, in
122    /// `0.0..=1.0`. The probabilities are normalised over the labels alone, so this is what tells
123    /// an answer the model gave from one read off letters it was not going to write.
124    pub fn label_mass(&self, id: &str) -> Result<f64, Error> {
125        Ok(self.get(id)?.label_mass())
126    }
127
128    /// The labels for `id` whose logprob is an upper bound rather than an observation. Empty
129    /// unless [`Answers::truncated`].
130    pub fn truncated_labels(&self, id: &str) -> Result<&[String], Error> {
131        Ok(self.get(id)?.truncated_labels())
132    }
133
134    pub fn ids(&self) -> impl Iterator<Item = &str> {
135        self.response.answers.keys().map(String::as_str)
136    }
137
138    pub fn into_inner(self) -> SystemOneResponse {
139        self.response
140    }
141
142    fn wrong_type(&self, id: &str, expected: &'static str, actual: &Answer) -> Error {
143        Error::WrongType {
144            id: id.to_string(),
145            expected,
146            actual: name_of(actual),
147        }
148    }
149}