use std::sync::Arc;
use async_trait::async_trait;
use synaptic_core::{ChatResponse, Embeddings, SynapticError};
use tokio::sync::RwLock;
use crate::LlmCache;
struct SemanticEntry {
embedding: Vec<f32>,
response: ChatResponse,
}
pub struct SemanticCache {
embeddings: Arc<dyn Embeddings>,
entries: RwLock<Vec<SemanticEntry>>,
similarity_threshold: f32,
}
impl SemanticCache {
pub fn new(embeddings: Arc<dyn Embeddings>, similarity_threshold: f32) -> Self {
Self {
embeddings,
entries: RwLock::new(Vec::new()),
similarity_threshold,
}
}
}
#[async_trait]
impl LlmCache for SemanticCache {
async fn get(&self, key: &str) -> Result<Option<ChatResponse>, SynapticError> {
let query_embedding =
self.embeddings.embed_query(key).await.map_err(|e| {
SynapticError::Cache(format!("embedding error during cache get: {e}"))
})?;
let entries = self.entries.read().await;
let mut best_score = f32::NEG_INFINITY;
let mut best_response = None;
for entry in entries.iter() {
let score = cosine_similarity(&query_embedding, &entry.embedding);
if score >= self.similarity_threshold && score > best_score {
best_score = score;
best_response = Some(entry.response.clone());
}
}
Ok(best_response)
}
async fn put(&self, key: &str, response: &ChatResponse) -> Result<(), SynapticError> {
let embedding =
self.embeddings.embed_query(key).await.map_err(|e| {
SynapticError::Cache(format!("embedding error during cache put: {e}"))
})?;
let mut entries = self.entries.write().await;
entries.push(SemanticEntry {
embedding,
response: response.clone(),
});
Ok(())
}
async fn clear(&self) -> Result<(), SynapticError> {
let mut entries = self.entries.write().await;
entries.clear();
Ok(())
}
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let mag_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let mag_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if mag_a == 0.0 || mag_b == 0.0 {
return 0.0;
}
dot / (mag_a * mag_b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cosine_similarity_identical_vectors() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
let sim = cosine_similarity(&a, &b);
assert!((sim - 1.0).abs() < 1e-6);
}
#[test]
fn cosine_similarity_orthogonal_vectors() {
let a = vec![1.0, 0.0];
let b = vec![0.0, 1.0];
let sim = cosine_similarity(&a, &b);
assert!(sim.abs() < 1e-6);
}
#[test]
fn cosine_similarity_empty_vectors() {
let a: Vec<f32> = vec![];
let b: Vec<f32> = vec![];
assert_eq!(cosine_similarity(&a, &b), 0.0);
}
}