use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use serde::{Deserialize, Serialize};
use crate::agentic::embedding::EmbeddingProvider;
use crate::agentic::tool_impl::ToolError;
use crate::types::ToolDefinition;
use crate::types::{Args, Tool};
use foundation_vectors::metric::DistanceMetric;
use foundation_vectors::store::{
InMemoryVectorStore, VectorEntry, VectorMetadata, VectorStore, VectorStoreConfig,
};
use foundation_vectors::vector::Vector;
pub const SHED_TOOL_NAME: &str = "shed";
pub const DEFAULT_SHED_LIMIT: usize = 5;
const NAMESPACE: &str = "tools";
#[must_use]
pub fn shed_definition() -> Tool {
let schema = serde_json::json!({
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "Natural language description of the tool you need"
},
"limit": {
"type": "integer",
"description": "Maximum number of tools to return",
"default": DEFAULT_SHED_LIMIT
}
},
"required": ["description"]
});
Tool::SingleCommand(ToolDefinition {
name: SHED_TOOL_NAME.into(),
description: "Find the tools available for a task. Describe what you need; \
every tool returned can be called from then on. Call a tool only \
after `shed` has returned it."
.into(),
arguments: Args::new(foundation_jsonschema::ValidationOptions::with_schema(
schema,
)),
category: "discovery".into(),
returns: None,
})
}
pub struct ToolDiscovery {
vector_store: OnceLock<Arc<dyn VectorStore>>,
embedder: Arc<dyn EmbeddingProvider>,
embedding_model: String,
summaries: std::sync::RwLock<HashMap<String, ToolSummary>>,
}
impl ToolDiscovery {
#[must_use]
pub fn new(
vector_store: Arc<dyn VectorStore>,
embedder: Arc<dyn EmbeddingProvider>,
embedding_model: String,
) -> Self {
Self {
vector_store: OnceLock::from(vector_store),
embedder,
embedding_model,
summaries: std::sync::RwLock::new(HashMap::new()),
}
}
#[must_use]
pub fn in_memory(embedder: Arc<dyn EmbeddingProvider>, embedding_model: String) -> Self {
Self {
vector_store: OnceLock::new(),
embedder,
embedding_model,
summaries: std::sync::RwLock::new(HashMap::new()),
}
}
fn embed(&self, text: &str) -> Result<Vec<f32>, ToolError> {
self.embedder
.embed(text, &self.embedding_model)
.map(|v| v.data)
.map_err(|e| ToolError::Execution {
tool: SHED_TOOL_NAME.into(),
reason: format!("embedding failed: {e}"),
})
}
pub fn index(&self, def: &ToolDefinition) -> Result<(), ToolError> {
self.index_tool(&Tool::SingleCommand(def.clone()))
}
pub fn index_tool(&self, tool: &Tool) -> Result<(), ToolError> {
let summary = ToolSummary::of(tool);
let embedding = self.embed(&format!("{}: {}", summary.name, summary.description))?;
let store = self.vector_store.get_or_init(|| {
Arc::new(InMemoryVectorStore::new(VectorStoreConfig::new(
embedding.len(),
DistanceMetric::Cosine,
)))
});
let mut metadata = VectorMetadata::default();
metadata.tags.insert("namespace".into(), NAMESPACE.into());
metadata
.tags
.insert("tool_name".into(), summary.name.clone());
store
.insert(
NAMESPACE,
VectorEntry {
id: format!("tool:{}", summary.name),
vector: Vector::new(embedding),
metadata,
},
)
.map_err(|e| ToolError::Execution {
tool: SHED_TOOL_NAME.into(),
reason: format!("vector insert failed: {e}"),
})?;
self.summaries
.write()
.expect("tool summaries poisoned")
.insert(summary.name.clone(), summary);
Ok(())
}
pub fn remove(&self, name: &str) -> Result<(), ToolError> {
let removed = self
.summaries
.write()
.expect("tool summaries poisoned")
.remove(name);
if removed.is_none() {
return Ok(());
}
match self.vector_store.get() {
Some(store) => store
.delete(NAMESPACE, &format!("tool:{name}"))
.map_err(|e| ToolError::Execution {
tool: SHED_TOOL_NAME.into(),
reason: format!("vector delete failed: {e}"),
}),
None => Ok(()),
}
}
pub fn search(&self, query: &str, k: usize) -> Result<Vec<ToolSummary>, ToolError> {
let Some(store) = self.vector_store.get() else {
return Ok(Vec::new());
};
let embedding = self.embed(query).map_err(|e| match e {
ToolError::Execution { tool, reason } => ToolError::Execution {
tool,
reason: format!("query {reason}"),
},
other => other,
})?;
let matches = store
.search(NAMESPACE, &embedding, k)
.map_err(|e| ToolError::Execution {
tool: SHED_TOOL_NAME.into(),
reason: format!("vector search failed: {e}"),
})?;
let summaries = self.summaries.read().expect("tool summaries poisoned");
Ok(matches
.iter()
.filter_map(|m| summaries.get(m.id.strip_prefix("tool:")?).cloned())
.collect())
}
}
#[derive(Serialize, Deserialize)]
pub struct ShedQuery {
pub description: String,
#[serde(default = "default_limit")]
pub limit: usize,
}
fn default_limit() -> usize {
DEFAULT_SHED_LIMIT
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShedResult {
pub tools: Vec<ToolSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSummary {
pub name: String,
pub description: String,
pub category: String,
pub schema: Option<serde_json::Value>,
}
impl ToolSummary {
#[must_use]
pub fn of(tool: &Tool) -> Self {
let spec = tool.function_spec();
Self {
name: spec.name,
description: spec.description,
category: tool.category().unwrap_or_default().to_string(),
schema: Some(spec.parameters),
}
}
}