use crate::{Error, ErrorKind, Question, Region};
use indexmap::IndexMap;
use serde_json::Value;
use std::sync::Arc;
pub type Probabilities = IndexMap<String, f64>;
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Call {
pub provider: String,
pub model: Option<String>,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AnswerValue {
Boolean(bool),
Choice(String),
Score(f64),
Box(Region),
}
pub trait AnswerType: Clone + Send + Sync + 'static {
fn from_answer_value(value: &AnswerValue) -> Result<Self, Error>;
}
macro_rules! typed {
($ty:ty,$variant:ident) => {
impl AnswerType for $ty {
fn from_answer_value(v: &AnswerValue) -> Result<Self, Error> {
match v {
AnswerValue::$variant(x) => Ok(x.clone()),
_ => Err(invalid("answer type does not match the requested output")),
}
}
}
};
}
typed!(bool, Boolean);
typed!(String, Choice);
typed!(f64, Score);
typed!(Region, Box);
impl AnswerType for AnswerValue {
fn from_answer_value(v: &AnswerValue) -> Result<Self, Error> {
Ok(v.clone())
}
}
#[derive(Clone, PartialEq)]
pub struct Answer<T = AnswerValue> {
value: Option<T>,
candidate: AnswerValue,
probabilities: Probabilities,
confidence: f64,
call: Option<Arc<Call>>,
raw: Option<Value>,
}
impl<T> std::fmt::Debug for Answer<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Answer")
.field("decided", &self.value.is_some())
.field("confidence", &self.confidence)
.field("call", &self.call)
.finish_non_exhaustive()
}
}
impl<T> Answer<T> {
pub fn value(&self) -> Option<&T> {
self.value.as_ref()
}
pub fn decided(&self) -> bool {
self.value.is_some()
}
pub fn confidence(&self) -> f64 {
self.confidence
}
pub fn probabilities(&self) -> &Probabilities {
&self.probabilities
}
pub fn call(&self) -> Option<&Arc<Call>> {
self.call.as_ref()
}
pub fn raw(&self) -> Option<&Value> {
self.raw.as_ref()
}
pub fn with_call(mut self, call: Arc<Call>) -> Self {
self.call = Some(call);
self
}
pub fn with_raw(mut self, raw: Value) -> Self {
self.raw = Some(raw);
self
}
pub fn with_min_confidence(mut self, threshold: f64) -> Result<Self, Error> {
if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
return Err(Error::new(
ErrorKind::InvalidArgument,
"threshold must be finite and from 0 to 1",
));
}
if self.confidence < threshold {
self.value = None;
}
Ok(self)
}
pub fn abstain(mut self) -> Self {
self.value = None;
self
}
}
impl Answer {
pub fn revalidate(&self, question: &Question) -> Result<Self, Error> {
let explicit = if matches!(question, Question::Box(_)) {
Some(self.confidence)
} else {
None
};
let mut answer = make_answer(
question,
self.candidate.clone(),
self.probabilities.clone(),
explicit,
)?;
if self.value.is_none() {
answer.value = None;
}
answer.call = self.call.clone();
answer.raw = self.raw.clone();
Ok(answer)
}
pub fn typed<T: AnswerType>(&self) -> Result<Answer<T>, Error> {
let candidate = T::from_answer_value(&self.candidate)?;
Ok(Answer {
value: self.value.as_ref().map(|_| candidate),
candidate: self.candidate.clone(),
probabilities: self.probabilities.clone(),
confidence: self.confidence,
call: self.call.clone(),
raw: self.raw.clone(),
})
}
}
fn invalid(msg: &str) -> Error {
Error::new(ErrorKind::InvalidResponse, msg)
}
pub fn make_answer(
question: &Question,
value: AnswerValue,
probabilities: Probabilities,
explicit_confidence: Option<f64>,
) -> Result<Answer, Error> {
if !matches!(question, Question::Box(_)) && explicit_confidence.is_some() {
return Err(invalid("explicit confidence is only valid for Box answers"));
}
let (confidence, tied) = match (question, &value) {
(Question::Boolean(_), AnswerValue::Boolean(v)) => {
distribution(&probabilities, &["true", "false"])?;
let yes = probabilities["true"];
let no = probabilities["false"];
let tied = yes == no;
if !tied && *v != (yes > no) {
return Err(invalid("Boolean value disagrees with its probabilities"));
}
(yes.max(no), tied)
}
(Question::Choice(q), AnswerValue::Choice(v)) => {
let labels: Vec<_> = q.options().keys().map(String::as_str).collect();
distribution(&probabilities, &labels)?;
let top = probabilities.values().copied().fold(0.0, f64::max);
if probabilities.get(v) != Some(&top) {
return Err(invalid("Choice value must name a most likely outcome"));
}
(
top,
probabilities.values().filter(|p| **p == top).count() > 1,
)
}
(Question::Score(q), AnswerValue::Score(v)) => {
let labels: Vec<_> = q.levels().iter().map(String::as_str).collect();
distribution(&probabilities, &labels)?;
let highest = q.levels().len() - 1;
if !v.is_finite() || *v < 0.0 || *v > highest as f64 {
return Err(invalid("Score value is outside the rubric"));
}
let expected = python_sum(
q.levels()
.iter()
.enumerate()
.map(|(i, label)| i as f64 * probabilities[label]),
);
let margin = 0.005 * (1.0 + (highest as f64 * (highest as f64 + 1.0) / 2.0));
if (*v - expected).abs() > margin {
return Err(invalid("Score value disagrees with its probabilities"));
}
let nearest = ((*v + 0.5).floor() as usize).min(highest);
(probabilities[&q.levels()[nearest]], false)
}
(Question::Box(_), AnswerValue::Box(_)) => {
if !probabilities.is_empty() {
return Err(invalid("Box answers have no probabilities"));
}
let c = explicit_confidence.unwrap_or(0.0);
if !c.is_finite() || !(0.0..=1.0).contains(&c) {
return Err(invalid("Box confidence must be finite and from 0 to 1"));
}
(c, false)
}
_ => return Err(invalid("answer value type does not match question")),
};
let decided = if tied || confidence < question.min_confidence() {
None
} else {
Some(value.clone())
};
Ok(Answer {
value: decided,
candidate: value,
probabilities,
confidence,
call: None,
raw: None,
})
}
fn distribution(p: &Probabilities, labels: &[&str]) -> Result<(), Error> {
if p.len() != labels.len() || labels.iter().any(|label| !p.contains_key(*label)) {
return Err(invalid(
"probabilities must cover exactly the question outcomes",
));
}
if p.values()
.any(|v| !v.is_finite() || !(0.0..=1.0).contains(v))
{
return Err(invalid("probabilities must be finite numbers from 0 to 1"));
}
if (accurate_sum(p.values().copied()) - 1.0).abs() > 0.005 * labels.len() as f64 + 1e-9 {
return Err(invalid("probabilities must sum to 1"));
}
Ok(())
}
fn accurate_sum(values: impl IntoIterator<Item = f64>) -> f64 {
let mut partials: Vec<f64> = Vec::new();
for mut x in values {
let mut i = 0;
for j in 0..partials.len() {
let mut y = partials[j];
if x.abs() < y.abs() {
std::mem::swap(&mut x, &mut y);
}
let hi = x + y;
let lo = y - (hi - x);
if lo != 0.0 {
partials[i] = lo;
i += 1;
}
x = hi;
}
partials.truncate(i);
partials.push(x);
}
let mut hi = partials.pop().unwrap_or(0.0);
let mut lo = 0.0;
while let Some(y) = partials.pop() {
let x = hi;
hi = x + y;
lo = y - (hi - x);
if lo != 0.0 {
break;
}
}
if partials
.last()
.is_some_and(|last| (lo < 0.0 && *last < 0.0) || (lo > 0.0 && *last > 0.0))
{
let y = lo * 2.0;
let x = hi + y;
if y == x - hi {
hi = x;
}
}
hi
}
pub fn complement(p: f64) -> Result<f64, Error> {
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(Error::new(
ErrorKind::InvalidArgument,
"probability must be finite and from 0 to 1",
));
}
format!("{:.10}", 1.0 - p)
.parse()
.map_err(|_| Error::new(ErrorKind::Runtime, "decimal conversion failed"))
}
pub fn token_count(value: &Value) -> Option<u64> {
value.as_u64()
}
fn python_sum(values: impl IntoIterator<Item = f64>) -> f64 {
let mut hi: f64 = 0.0;
let mut lo = 0.0;
for x in values {
let t = hi + x;
lo += if hi.abs() >= x.abs() {
(hi - t) + x
} else {
(x - t) + hi
};
hi = t;
}
hi + lo
}