use crate::each::{BatchOptions, ItemResult, ask_each, validate_ids};
use crate::{Answer, AnswerValue, Error, ErrorKind, Provider, Result, Score, State};
use indexmap::IndexMap;
use serde_json::{Map, Value};
use std::collections::BTreeSet;
#[derive(Clone)]
pub struct Candidate {
id: String,
document: Value,
}
impl Candidate {
pub fn new(id: impl Into<String>, document: Value) -> Result<Self> {
let id = id.into();
validate_ids([id.as_str()])?;
Ok(Self { id, document })
}
pub fn id(&self) -> &str {
&self.id
}
pub fn document(&self) -> &Value {
&self.document
}
}
impl std::fmt::Debug for Candidate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Candidate").finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct Ranked {
id: String,
answer: Answer<AnswerValue>,
score: f64,
position: usize,
}
impl Ranked {
pub fn id(&self) -> &str {
&self.id
}
pub fn answer(&self) -> &Answer<AnswerValue> {
&self.answer
}
pub fn score(&self) -> f64 {
self.score
}
pub fn position(&self) -> usize {
self.position
}
}
#[derive(Clone, Debug)]
pub struct Ranking {
question: Score,
ranked: Vec<Ranked>,
unscored: IndexMap<String, ItemResult>,
}
impl Ranking {
pub fn question(&self) -> &Score {
&self.question
}
pub fn ranked(&self) -> &[Ranked] {
&self.ranked
}
pub fn unscored(&self) -> &IndexMap<String, ItemResult> {
&self.unscored
}
pub fn complete(&self) -> bool {
self.unscored.is_empty()
}
pub fn models(&self) -> BTreeSet<&str> {
self.ranked
.iter()
.map(|r| &r.answer)
.chain(self.unscored.values().filter_map(ItemResult::answer))
.filter_map(|a| a.call())
.filter_map(|c| c.model.as_deref())
.filter(|m| !m.is_empty())
.collect()
}
pub fn top(&self, k: usize, allow_partial: bool) -> Result<&[Ranked]> {
if !self.complete() && !allow_partial {
return Err(invalid(
"ranking has unscored candidates; explicitly allow partial results",
));
}
if self.models().len() > 1 {
return Err(invalid(
"ranking contains answers from different reported models",
));
}
Ok(&self.ranked[..k.min(self.ranked.len())])
}
pub fn reweighted(&self, weights: &[f64]) -> Result<Self> {
if weights.len() != self.question.levels().len()
|| weights.iter().any(|w| !w.is_finite())
|| weights.windows(2).any(|w| w[0] >= w[1])
{
return Err(invalid(
"need one finite strictly increasing weight per rubric level",
));
}
let mut ranked = Vec::with_capacity(self.ranked.len());
for old in &self.ranked {
let probabilities: Vec<_> = self
.question
.levels()
.iter()
.map(|label| old.answer.probabilities()[label])
.collect();
let total = accurate_sum(probabilities.iter().copied())?;
let weighted = accurate_sum(weights.iter().zip(&probabilities).map(|(w, p)| w * p))?;
let score = weighted / total;
if !score.is_finite() {
return Err(invalid("reweighted score is not finite"));
}
ranked.push(Ranked {
id: old.id.clone(),
answer: old.answer.clone(),
score,
position: old.position,
});
}
sort(&mut ranked);
Ok(Self {
question: self.question.clone(),
ranked,
unscored: self.unscored.clone(),
})
}
}
pub async fn rerank(
provider: &dyn Provider,
question: &Score,
query: &str,
candidates: &[Candidate],
options: &BatchOptions,
) -> Result<Ranking> {
if crate::question::is_blank(query) {
return Err(invalid("query must be a non-empty string"));
}
validate_ids(candidates.iter().map(Candidate::id))?;
let mut states = Vec::with_capacity(candidates.len());
for candidate in candidates {
let mut value = Map::new();
value.insert("query".into(), Value::String(query.into()));
value.insert("document".into(), candidate.document.clone());
if let Some(context) = options.context.as_ref().filter(|v| !v.is_null()) {
value.insert("context".into(), context.clone());
}
states.push(State::try_from(Value::Object(value))?);
}
let results = ask_each(provider, &question.clone().into(), &states, options).await?;
let mut ranked = Vec::new();
let mut unscored = IndexMap::new();
for (position, (candidate, result)) in candidates.iter().zip(results).enumerate() {
match result {
ItemResult::Answer(answer) if answer.decided() => {
let Some(AnswerValue::Score(score)) = answer.value() else {
return Err(Error::new(
ErrorKind::InvalidResponse,
"rerank answer is not a score",
));
};
ranked.push(Ranked {
id: candidate.id.clone(),
score: *score,
answer,
position,
});
}
other => {
unscored.insert(candidate.id.clone(), other);
}
}
}
sort(&mut ranked);
Ok(Ranking {
question: question.clone(),
ranked,
unscored,
})
}
pub fn reweighted(ranking: &Ranking, weights: &[f64]) -> Result<Ranking> {
ranking.reweighted(weights)
}
fn sort(items: &mut [Ranked]) {
items.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.position.cmp(&b.position))
});
}
fn invalid(message: &str) -> Error {
Error::new(ErrorKind::InvalidArgument, message)
}
fn accurate_sum(values: impl IntoIterator<Item = f64>) -> Result<f64> {
let mut partials: Vec<f64> = Vec::new();
for mut x in values {
if !x.is_finite() {
return Err(invalid("reweighted sum overflow"));
}
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;
if !hi.is_finite() {
return Err(invalid("reweighted sum overflow"));
}
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 let Some(last) = partials.last() {
if (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;
}
}
}
Ok(hi)
}