use aphrody_rag_core::fusion::l2_normalize;
use async_trait::async_trait;
use super::Embedder;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct StubEmbedder {
model_name: String,
dimension: usize,
}
impl Default for StubEmbedder {
fn default() -> Self {
Self {
model_name: "stub-384".to_string(),
dimension: 384,
}
}
}
impl StubEmbedder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_model(model_name: impl Into<String>) -> Self {
Self {
model_name: model_name.into(),
dimension: 384,
}
}
#[must_use]
pub fn with_model_and_dim(model_name: impl Into<String>, dimension: usize) -> Self {
Self {
model_name: model_name.into(),
dimension,
}
}
pub fn embed_one_dim(text: &str, dim: usize) -> Vec<f32> {
let mut out = Vec::with_capacity(dim);
let mut state = blake3::hash(text.as_bytes());
let chunks_needed = dim.div_ceil(8);
for i in 0..chunks_needed {
let bytes = state.as_bytes();
for chunk in bytes.as_chunks::<4>().0.iter() {
if out.len() >= dim {
break;
}
let u = u32::from_le_bytes(*chunk);
let f = ((u as f64 / u32::MAX as f64) * 2.0 - 1.0) as f32;
out.push(f);
}
state = blake3::hash(&[bytes.as_slice(), &(i as u32).to_le_bytes()].concat());
}
out.truncate(dim);
l2_normalize(&mut out);
out
}
pub fn embed_one(text: &str) -> Vec<f32> {
Self::embed_one_dim(text, 384)
}
}
#[async_trait]
impl Embedder for StubEmbedder {
fn model_id(&self) -> &str {
&self.model_name
}
fn dimension(&self) -> usize {
self.dimension
}
async fn embed(&self, texts: &[String], is_query: bool) -> Result<Vec<Vec<f32>>> {
let results = texts
.iter()
.map(|t| {
let formatted = super::format_query_or_passage(&self.model_name, t, is_query);
Self::embed_one_dim(&formatted, self.dimension)
})
.collect();
Ok(results)
}
}
#[cfg(test)]
mod tests {
use aphrody_rag_core::fusion::cosine;
use super::*;
#[tokio::test]
async fn test_stub_embedder_deterministic_and_normalized() {
let embedder = StubEmbedder::new();
let texts = vec![
"Hello world".to_string(),
"Aphrody autonomous agent".to_string(),
];
let res = embedder.embed(&texts, false).await.expect("embed ok");
assert_eq!(res.len(), 2);
assert_eq!(res[0].len(), 384);
assert_eq!(res[1].len(), 384);
let sim = cosine(&res[0], &res[0]);
assert!((sim - 1.0).abs() < 1e-5);
let cross = cosine(&res[0], &res[1]);
assert!(cross < 0.9);
}
}