use std::collections::HashSet;
pub trait Embedder {
fn embed(&self, s: &str) -> Vec<f32>;
}
#[derive(Debug, Clone)]
pub struct ToolAnchor {
pub name: String,
pub embedding: Vec<f32>,
}
impl ToolAnchor {
pub fn build<E: Embedder>(
name: &str,
example_queries: &[&str],
description_fallback: &str,
embedder: &E,
) -> Self {
let text = if example_queries.is_empty() {
description_fallback.to_string()
} else {
example_queries.join(" • ")
};
Self {
name: name.to_string(),
embedding: embedder.embed(&text),
}
}
}
pub fn cosine(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
pub fn retrieve_top_k(query_embedding: &[f32], anchors: &[ToolAnchor], k: usize) -> Vec<String> {
let mut scored: Vec<(f32, &str)> = anchors
.iter()
.map(|a| (cosine(query_embedding, &a.embedding), a.name.as_str()))
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
scored
.into_iter()
.take(k)
.map(|(_, name)| name.to_string())
.collect()
}
pub fn filter_tools_by_name<T, F>(tools: Vec<T>, keep: &[String], get_name: F) -> Vec<T>
where
F: Fn(&T) -> &str,
{
let keep_set: HashSet<&str> = keep.iter().map(|s| s.as_str()).collect();
tools
.into_iter()
.filter(|t| keep_set.contains(get_name(t)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
struct StubEmbedder;
impl Embedder for StubEmbedder {
fn embed(&self, s: &str) -> Vec<f32> {
let mut v = [0f32; 8];
for (i, b) in s.bytes().enumerate() {
v[i % 8] += b as f32;
}
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-6);
v.iter().map(|x| x / norm).collect()
}
}
#[test]
fn cosine_normalised_vectors_in_range() {
let a = StubEmbedder.embed("hello world");
let b = StubEmbedder.embed("hello there");
let s = cosine(&a, &b);
assert!((-1.0..=1.0 + 1e-5).contains(&s));
}
#[test]
fn retrieve_top_k_returns_in_descending_similarity_order() {
let anchors = vec![
ToolAnchor::build("read_file", &["read a file"], "", &StubEmbedder),
ToolAnchor::build("list_dir", &["list a directory"], "", &StubEmbedder),
ToolAnchor::build("write_file", &["write a file"], "", &StubEmbedder),
];
let q = StubEmbedder.embed("read a file please");
let top = retrieve_top_k(&q, &anchors, 3);
assert_eq!(top.len(), 3);
let scored: Vec<f32> = top
.iter()
.map(|name| {
let a = anchors.iter().find(|a| a.name == *name).unwrap();
cosine(&q, &a.embedding)
})
.collect();
for w in scored.windows(2) {
assert!(
w[0] >= w[1] - 1e-5,
"scores must be sorted descending: {:?}",
scored
);
}
}
#[test]
fn retrieve_top_k_clamped_to_anchor_count() {
let anchors = vec![ToolAnchor::build(
"only",
&["the only tool"],
"",
&StubEmbedder,
)];
let q = StubEmbedder.embed("anything");
let top = retrieve_top_k(&q, &anchors, 100);
assert_eq!(top.len(), 1);
assert_eq!(top[0], "only");
}
#[test]
fn filter_tools_preserves_original_order() {
#[derive(Debug, Clone, PartialEq)]
struct Tool(&'static str);
let tools = vec![Tool("a"), Tool("b"), Tool("c"), Tool("d")];
let keep = vec!["c".to_string(), "a".to_string()];
let filtered = filter_tools_by_name(tools, &keep, |t| t.0);
assert_eq!(filtered, vec![Tool("a"), Tool("c")]);
}
#[test]
fn filter_tools_empty_keep_filters_everything() {
#[derive(Debug, Clone, PartialEq)]
struct Tool(&'static str);
let tools = vec![Tool("a"), Tool("b")];
let keep: Vec<String> = vec![];
let filtered = filter_tools_by_name(tools, &keep, |t| t.0);
assert!(filtered.is_empty());
}
#[test]
fn empty_example_queries_fall_back_to_description() {
let a = ToolAnchor::build("x", &[], "fallback description", &StubEmbedder);
assert_eq!(a.embedding.len(), 8);
}
}