use std::future::Future;
use std::pin::Pin;
use serde::{Deserialize, Serialize};
use crate::error::Result;
pub mod answer_relevance;
pub mod context_precision;
pub mod context_recall;
pub mod faithfulness;
pub mod harness;
pub use answer_relevance::{AnswerRelevanceMetric, HypotheticalQuestions};
pub use context_precision::{ContextPrecisionMetric, ContextRelevance};
pub use context_recall::ContextRecallMetric;
pub use faithfulness::{Claim, ClaimAttribution, Claims, FaithfulnessMetric};
pub use harness::RagasHarness;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagasInputs {
pub query_id: String,
pub query: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub answer: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reference_answer: Option<String>,
}
impl RagasInputs {
#[must_use]
pub fn new(
query_id: impl Into<String>,
query: impl Into<String>,
answer: impl Into<String>,
context: Vec<String>,
) -> Self {
Self {
query_id: query_id.into(),
query: query.into(),
answer: Some(answer.into()),
context,
reference_answer: None,
}
}
#[must_use]
pub fn with_reference(mut self, reference: impl Into<String>) -> Self {
self.reference_answer = Some(reference.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagasScore {
pub value: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rationales: Vec<String>,
}
impl RagasScore {
#[must_use]
pub fn measured(value: f64) -> Self {
Self {
value: Some(value),
rationales: Vec::new(),
}
}
#[must_use]
pub fn with_rationales(value: f64, rationales: Vec<String>) -> Self {
Self {
value: Some(value),
rationales,
}
}
#[must_use]
pub fn not_measurable(reason: impl Into<String>) -> Self {
Self {
value: None,
rationales: vec![reason.into()],
}
}
}
pub trait RagasMetric: Send + Sync {
fn name(&self) -> &'static str;
fn fingerprint_component(&self) -> String;
fn score(&self, inputs: &RagasInputs) -> impl Future<Output = Result<RagasScore>> + Send;
}
pub trait DynRagasMetric: Send + Sync {
fn name(&self) -> &'static str;
fn fingerprint_component(&self) -> String;
fn score<'a>(
&'a self,
inputs: &'a RagasInputs,
) -> Pin<Box<dyn Future<Output = Result<RagasScore>> + Send + 'a>>;
}
impl<M: RagasMetric> DynRagasMetric for M {
fn name(&self) -> &'static str {
RagasMetric::name(self)
}
fn fingerprint_component(&self) -> String {
RagasMetric::fingerprint_component(self)
}
fn score<'a>(
&'a self,
inputs: &'a RagasInputs,
) -> Pin<Box<dyn Future<Output = Result<RagasScore>> + Send + 'a>> {
Box::pin(RagasMetric::score(self, inputs))
}
}
#[must_use]
pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let mut dot = 0.0;
let mut na = 0.0;
let mut nb = 0.0;
for (x, y) in a.iter().zip(b.iter()) {
dot += x * y;
na += x * x;
nb += y * y;
}
if na == 0.0 || nb == 0.0 {
return 0.0;
}
dot / (na.sqrt() * nb.sqrt())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn cosine_orthogonal_is_zero() {
assert!((cosine_similarity(&[1.0, 0.0], &[0.0, 1.0]) - 0.0).abs() < 1e-12);
}
#[test]
fn cosine_identical_is_one() {
assert!((cosine_similarity(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0]) - 1.0).abs() < 1e-12);
}
#[test]
fn cosine_mismatched_length_is_zero() {
assert_eq!(cosine_similarity(&[1.0], &[1.0, 2.0]), 0.0);
}
#[test]
fn cosine_zero_vector_is_zero() {
assert_eq!(cosine_similarity(&[0.0, 0.0], &[1.0, 1.0]), 0.0);
}
#[test]
fn not_measurable_carries_reason() {
let s = RagasScore::not_measurable("empty answer");
assert!(s.value.is_none());
assert_eq!(s.rationales, vec!["empty answer".to_string()]);
}
}