mod types;
pub use types::*;
use async_trait::async_trait;
use serde_json::Value;
use crate::error::Result;
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let mut dot = 0.0f32;
let mut norm_a = 0.0f32;
let mut norm_b = 0.0f32;
for (x, y) in a.iter().zip(b.iter()) {
dot += x * y;
norm_a += x * x;
norm_b += y * y;
}
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
dot / (norm_a.sqrt() * norm_b.sqrt())
}
impl InMemoryVectorStore {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.inner
.lock()
.map(|inner| inner.entries.len())
.unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
fn store_lock_err(e: impl std::fmt::Display) -> crate::error::TinyAgentsError {
crate::error::TinyAgentsError::Embedding(format!("vector store lock poisoned: {e}"))
}
#[async_trait]
impl VectorStore for InMemoryVectorStore {
async fn add(&self, id: String, vector: Vec<f32>, metadata: Value) -> Result<()> {
if vector.is_empty() {
return Err(crate::error::TinyAgentsError::Validation(
"cannot add a zero-dimensional vector to the vector store".to_string(),
));
}
let mut inner = self.inner.lock().map_err(store_lock_err)?;
if let Some(first) = inner.entries.first()
&& first.vector.len() != vector.len()
{
return Err(crate::error::TinyAgentsError::Validation(format!(
"vector for id `{id}` has {} dimensions but the store holds {}-dimensional vectors",
vector.len(),
first.vector.len()
)));
}
match inner.index.get(&id) {
Some(&at) => {
let existing = &mut inner.entries[at];
existing.vector = vector;
existing.metadata = metadata;
}
None => {
let at = inner.entries.len();
inner.index.insert(id.clone(), at);
inner.entries.push(VectorEntry {
id,
vector,
metadata,
});
}
}
Ok(())
}
async fn query(&self, vector: &[f32], top_k: usize) -> Result<Vec<ScoredDoc>> {
if top_k == 0 {
return Ok(Vec::new());
}
let inner = self.inner.lock().map_err(store_lock_err)?;
let Some(first) = inner.entries.first() else {
return Ok(Vec::new());
};
if vector.len() != first.vector.len() {
return Err(crate::error::TinyAgentsError::Validation(format!(
"query vector has {} dimensions but the store holds {}-dimensional vectors",
vector.len(),
first.vector.len()
)));
}
let mut scored: Vec<(usize, f32)> = inner
.entries
.iter()
.enumerate()
.map(|(at, e)| (at, cosine_similarity(vector, &e.vector)))
.collect();
scored.sort_by(|a, b| b.1.total_cmp(&a.1));
scored.truncate(top_k);
Ok(scored
.into_iter()
.map(|(at, score)| {
let entry = &inner.entries[at];
ScoredDoc {
id: entry.id.clone(),
score,
metadata: entry.metadata.clone(),
}
})
.collect())
}
}
impl Retriever {
pub fn new(
model: std::sync::Arc<dyn EmbeddingModel>,
store: std::sync::Arc<dyn VectorStore>,
) -> Self {
Self { model, store }
}
pub fn model(&self) -> &std::sync::Arc<dyn EmbeddingModel> {
&self.model
}
pub fn store(&self) -> &std::sync::Arc<dyn VectorStore> {
&self.store
}
pub async fn index(&self, docs: Vec<(String, String, Value)>) -> Result<()> {
if docs.is_empty() {
return Ok(());
}
let texts: Vec<String> = docs.iter().map(|(_, text, _)| text.clone()).collect();
let vectors = self.model.embed(&texts).await?;
for ((id, _text, metadata), vector) in docs.into_iter().zip(vectors) {
self.store.add(id, vector, metadata).await?;
}
Ok(())
}
pub async fn retrieve(&self, query: &str, top_k: usize) -> Result<Vec<ScoredDoc>> {
let query_vector = self.model.embed_query(query).await?;
self.store.query(&query_vector, top_k).await
}
}
mod cloud;
mod cohere;
mod noop;
mod ollama;
mod openai;
mod rate_limit;
mod retry_after;
mod voyage;
pub use noop::NoopEmbeddingModel;
pub use ollama::{
DEFAULT_OLLAMA_DIMENSIONS, DEFAULT_OLLAMA_MODEL, DEFAULT_OLLAMA_URL, OllamaEmbeddingModel,
};
pub use openai::OpenAiEmbeddingModel;
pub use rate_limit::{DEFAULT_REQUESTS_PER_MINUTE, acquire, rate_limit, set_rate_limit};
pub use retry_after::{
BASE_BACKOFF_MS, MAX_BACKOFF_MS, MAX_RETRIES, backoff_ms_for_attempt, parse_retry_after_ms,
};
pub use types::format_embedding_signature;
pub use voyage::{
VOYAGE_API_BASE, VOYAGE_DEFAULT_DIMENSIONS, VOYAGE_DEFAULT_MODEL, VoyageEmbeddingModel,
};
#[cfg(test)]
mod test;
pub use cloud::{
BearerResolver, CloudEmbeddingModel, DEFAULT_CLOUD_DIMENSIONS, DEFAULT_CLOUD_MODEL,
};
pub use cohere::{
COHERE_API_BASE, COHERE_DEFAULT_DIMENSIONS, COHERE_DEFAULT_MODEL, CohereEmbeddingModel,
};