use std::sync::Arc;
use adk_memory::EmbeddingProvider;
use crate::error::{EvalError, Result};
pub struct EmbeddingScorer {
provider: Arc<dyn EmbeddingProvider>,
}
impl EmbeddingScorer {
pub fn new(provider: Arc<dyn EmbeddingProvider>) -> Self {
Self { provider }
}
pub async fn score(&self, expected: &str, actual: &str) -> Result<f64> {
let texts = vec![expected.to_string(), actual.to_string()];
let embeddings =
self.provider.embed(&texts).await.map_err(|e| {
EvalError::EmbeddingError(format!("embedding generation failed: {e}"))
})?;
if embeddings.len() < 2 {
return Err(EvalError::EmbeddingError(
"provider returned fewer than 2 embeddings".to_string(),
));
}
let expected_vec = &embeddings[0];
let actual_vec = &embeddings[1];
if expected_vec.len() != actual_vec.len() {
return Err(EvalError::EmbeddingError(format!(
"dimension mismatch: expected vector has {} dimensions, actual has {}",
expected_vec.len(),
actual_vec.len()
)));
}
Ok(cosine_similarity(expected_vec, actual_vec))
}
}
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let mut dot_product = 0.0_f64;
let mut magnitude_a = 0.0_f64;
let mut magnitude_b = 0.0_f64;
for (ai, bi) in a.iter().zip(b.iter()) {
let ai_f64 = f64::from(*ai);
let bi_f64 = f64::from(*bi);
dot_product += ai_f64 * bi_f64;
magnitude_a += ai_f64 * ai_f64;
magnitude_b += bi_f64 * bi_f64;
}
let magnitude_a = magnitude_a.sqrt();
let magnitude_b = magnitude_b.sqrt();
if magnitude_a == 0.0 || magnitude_b == 0.0 {
return 0.0;
}
let similarity = dot_product / (magnitude_a * magnitude_b);
similarity.clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cosine_similarity_identical_vectors() {
let v = vec![1.0_f32, 2.0, 3.0];
let score = cosine_similarity(&v, &v);
assert!((score - 1.0).abs() < 1e-10);
}
#[test]
fn test_cosine_similarity_orthogonal_vectors() {
let a = vec![1.0_f32, 0.0];
let b = vec![0.0_f32, 1.0];
let score = cosine_similarity(&a, &b);
assert!(score.abs() < 1e-10);
}
#[test]
fn test_cosine_similarity_zero_vector() {
let a = vec![1.0_f32, 2.0, 3.0];
let zero = vec![0.0_f32, 0.0, 0.0];
assert_eq!(cosine_similarity(&a, &zero), 0.0);
assert_eq!(cosine_similarity(&zero, &a), 0.0);
assert_eq!(cosine_similarity(&zero, &zero), 0.0);
}
#[test]
fn test_cosine_similarity_mismatched_dimensions() {
let a = vec![1.0_f32, 2.0];
let b = vec![1.0_f32, 2.0, 3.0];
assert_eq!(cosine_similarity(&a, &b), 0.0);
}
#[test]
fn test_cosine_similarity_empty_vectors() {
let empty: Vec<f32> = vec![];
assert_eq!(cosine_similarity(&empty, &empty), 0.0);
}
#[test]
fn test_cosine_similarity_opposite_vectors_clamped() {
let a = vec![1.0_f32, 0.0];
let b = vec![-1.0_f32, 0.0];
let score = cosine_similarity(&a, &b);
assert_eq!(score, 0.0);
}
#[test]
fn test_cosine_similarity_result_in_range() {
let a = vec![1.0_f32, 2.0, 3.0, 4.0];
let b = vec![4.0_f32, 3.0, 2.0, 1.0];
let score = cosine_similarity(&a, &b);
assert!((0.0..=1.0).contains(&score));
}
}