1use crate::{Error, ErrorKind, Question, Region};
3use indexmap::IndexMap;
4use serde_json::Value;
5use std::sync::Arc;
6pub type Probabilities = IndexMap<String, f64>;
8#[derive(Clone, Debug, PartialEq, Eq, Default)]
10pub struct Call {
11 pub provider: String,
13 pub model: Option<String>,
15 pub input_tokens: Option<u64>,
17 pub output_tokens: Option<u64>,
19}
20#[derive(Clone, Debug, PartialEq)]
22pub enum AnswerValue {
23 Boolean(bool),
25 Choice(String),
27 Score(f64),
29 Box(Region),
31}
32pub trait AnswerType: Clone + Send + Sync + 'static {
34 fn from_answer_value(value: &AnswerValue) -> Result<Self, Error>;
40}
41macro_rules! typed {
42 ($ty:ty,$variant:ident) => {
43 impl AnswerType for $ty {
44 fn from_answer_value(v: &AnswerValue) -> Result<Self, Error> {
45 match v {
46 AnswerValue::$variant(x) => Ok(x.clone()),
47 _ => Err(invalid("answer type does not match the requested output")),
48 }
49 }
50 }
51 };
52}
53typed!(bool, Boolean);
54typed!(String, Choice);
55typed!(f64, Score);
56typed!(Region, Box);
57impl AnswerType for AnswerValue {
58 fn from_answer_value(v: &AnswerValue) -> Result<Self, Error> {
59 Ok(v.clone())
60 }
61}
62#[derive(Clone, PartialEq)]
64pub struct Answer<T = AnswerValue> {
65 value: Option<T>,
66 candidate: AnswerValue,
67 probabilities: Probabilities,
68 confidence: f64,
69 call: Option<Arc<Call>>,
70 raw: Option<Value>,
71}
72impl<T> std::fmt::Debug for Answer<T> {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("Answer")
75 .field("decided", &self.value.is_some())
76 .field("confidence", &self.confidence)
77 .field("call", &self.call)
78 .finish_non_exhaustive()
79 }
80}
81impl<T> Answer<T> {
82 pub fn value(&self) -> Option<&T> {
84 self.value.as_ref()
85 }
86 pub fn decided(&self) -> bool {
88 self.value.is_some()
89 }
90 pub fn confidence(&self) -> f64 {
92 self.confidence
93 }
94 pub fn probabilities(&self) -> &Probabilities {
96 &self.probabilities
97 }
98 pub fn call(&self) -> Option<&Arc<Call>> {
100 self.call.as_ref()
101 }
102 pub fn raw(&self) -> Option<&Value> {
104 self.raw.as_ref()
105 }
106 pub fn with_call(mut self, call: Arc<Call>) -> Self {
108 self.call = Some(call);
109 self
110 }
111 pub fn with_raw(mut self, raw: Value) -> Self {
113 self.raw = Some(raw);
114 self
115 }
116 pub fn with_min_confidence(mut self, threshold: f64) -> Result<Self, Error> {
121 if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
122 return Err(Error::new(
123 ErrorKind::InvalidArgument,
124 "threshold must be finite and from 0 to 1",
125 ));
126 }
127 if self.confidence < threshold {
128 self.value = None;
129 }
130 Ok(self)
131 }
132 pub fn abstain(mut self) -> Self {
134 self.value = None;
135 self
136 }
137}
138impl Answer {
139 pub fn revalidate(&self, question: &Question) -> Result<Self, Error> {
146 let explicit = if matches!(question, Question::Box(_)) {
147 Some(self.confidence)
148 } else {
149 None
150 };
151 let mut answer = make_answer(
152 question,
153 self.candidate.clone(),
154 self.probabilities.clone(),
155 explicit,
156 )?;
157 if self.value.is_none() {
158 answer.value = None;
159 }
160 answer.call = self.call.clone();
161 answer.raw = self.raw.clone();
162 Ok(answer)
163 }
164 pub fn typed<T: AnswerType>(&self) -> Result<Answer<T>, Error> {
170 let candidate = T::from_answer_value(&self.candidate)?;
171 Ok(Answer {
172 value: self.value.as_ref().map(|_| candidate),
173 candidate: self.candidate.clone(),
174 probabilities: self.probabilities.clone(),
175 confidence: self.confidence,
176 call: self.call.clone(),
177 raw: self.raw.clone(),
178 })
179 }
180}
181fn invalid(msg: &str) -> Error {
182 Error::new(ErrorKind::InvalidResponse, msg)
183}
184pub fn make_answer(
210 question: &Question,
211 value: AnswerValue,
212 probabilities: Probabilities,
213 explicit_confidence: Option<f64>,
214) -> Result<Answer, Error> {
215 if !matches!(question, Question::Box(_)) && explicit_confidence.is_some() {
216 return Err(invalid("explicit confidence is only valid for Box answers"));
217 }
218 let (confidence, tied) = match (question, &value) {
219 (Question::Boolean(_), AnswerValue::Boolean(v)) => {
220 distribution(&probabilities, &["true", "false"])?;
221 let yes = probabilities["true"];
222 let no = probabilities["false"];
223 let tied = yes == no;
224 if !tied && *v != (yes > no) {
225 return Err(invalid("Boolean value disagrees with its probabilities"));
226 }
227 (yes.max(no), tied)
228 }
229 (Question::Choice(q), AnswerValue::Choice(v)) => {
230 let labels: Vec<_> = q.options().keys().map(String::as_str).collect();
231 distribution(&probabilities, &labels)?;
232 let top = probabilities.values().copied().fold(0.0, f64::max);
233 if probabilities.get(v) != Some(&top) {
234 return Err(invalid("Choice value must name a most likely outcome"));
235 }
236 (
237 top,
238 probabilities.values().filter(|p| **p == top).count() > 1,
239 )
240 }
241 (Question::Score(q), AnswerValue::Score(v)) => {
242 let labels: Vec<_> = q.levels().iter().map(String::as_str).collect();
243 distribution(&probabilities, &labels)?;
244 let highest = q.levels().len() - 1;
245 if !v.is_finite() || *v < 0.0 || *v > highest as f64 {
246 return Err(invalid("Score value is outside the rubric"));
247 }
248 let expected = python_sum(
249 q.levels()
250 .iter()
251 .enumerate()
252 .map(|(i, label)| i as f64 * probabilities[label]),
253 );
254 let margin = 0.005 * (1.0 + (highest as f64 * (highest as f64 + 1.0) / 2.0));
255 if (*v - expected).abs() > margin {
256 return Err(invalid("Score value disagrees with its probabilities"));
257 }
258 let nearest = ((*v + 0.5).floor() as usize).min(highest);
259 (probabilities[&q.levels()[nearest]], false)
260 }
261 (Question::Box(_), AnswerValue::Box(_)) => {
262 if !probabilities.is_empty() {
263 return Err(invalid("Box answers have no probabilities"));
264 }
265 let c = explicit_confidence.unwrap_or(0.0);
266 if !c.is_finite() || !(0.0..=1.0).contains(&c) {
267 return Err(invalid("Box confidence must be finite and from 0 to 1"));
268 }
269 (c, false)
270 }
271 _ => return Err(invalid("answer value type does not match question")),
272 };
273 let decided = if tied || confidence < question.min_confidence() {
274 None
275 } else {
276 Some(value.clone())
277 };
278 Ok(Answer {
279 value: decided,
280 candidate: value,
281 probabilities,
282 confidence,
283 call: None,
284 raw: None,
285 })
286}
287fn distribution(p: &Probabilities, labels: &[&str]) -> Result<(), Error> {
288 if p.len() != labels.len() || labels.iter().any(|label| !p.contains_key(*label)) {
289 return Err(invalid(
290 "probabilities must cover exactly the question outcomes",
291 ));
292 }
293 if p.values()
294 .any(|v| !v.is_finite() || !(0.0..=1.0).contains(v))
295 {
296 return Err(invalid("probabilities must be finite numbers from 0 to 1"));
297 }
298 if (accurate_sum(p.values().copied()) - 1.0).abs() > 0.005 * labels.len() as f64 + 1e-9 {
299 return Err(invalid("probabilities must sum to 1"));
300 }
301 Ok(())
302}
303fn accurate_sum(values: impl IntoIterator<Item = f64>) -> f64 {
306 let mut partials: Vec<f64> = Vec::new();
307 for mut x in values {
308 let mut i = 0;
309 for j in 0..partials.len() {
310 let mut y = partials[j];
311 if x.abs() < y.abs() {
312 std::mem::swap(&mut x, &mut y);
313 }
314 let hi = x + y;
315 let lo = y - (hi - x);
316 if lo != 0.0 {
317 partials[i] = lo;
318 i += 1;
319 }
320 x = hi;
321 }
322 partials.truncate(i);
323 partials.push(x);
324 }
325 let mut hi = partials.pop().unwrap_or(0.0);
326 let mut lo = 0.0;
327 while let Some(y) = partials.pop() {
328 let x = hi;
329 hi = x + y;
330 lo = y - (hi - x);
331 if lo != 0.0 {
332 break;
333 }
334 }
335 if partials
336 .last()
337 .is_some_and(|last| (lo < 0.0 && *last < 0.0) || (lo > 0.0 && *last > 0.0))
338 {
339 let y = lo * 2.0;
340 let x = hi + y;
341 if y == x - hi {
342 hi = x;
343 }
344 }
345 hi
346}
347pub fn complement(p: f64) -> Result<f64, Error> {
354 if !p.is_finite() || !(0.0..=1.0).contains(&p) {
355 return Err(Error::new(
356 ErrorKind::InvalidArgument,
357 "probability must be finite and from 0 to 1",
358 ));
359 }
360 format!("{:.10}", 1.0 - p)
363 .parse()
364 .map_err(|_| Error::new(ErrorKind::Runtime, "decimal conversion failed"))
365}
366pub fn token_count(value: &Value) -> Option<u64> {
368 value.as_u64()
369}
370
371fn python_sum(values: impl IntoIterator<Item = f64>) -> f64 {
373 let mut hi: f64 = 0.0;
374 let mut lo = 0.0;
375 for x in values {
376 let t = hi + x;
377 lo += if hi.abs() >= x.abs() {
378 (hi - t) + x
379 } else {
380 (x - t) + hi
381 };
382 hi = t;
383 }
384 hi + lo
385}