use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use super::{Tool, ToolContext, ToolError, ToolOutput, ToolSchema};
pub struct ToolRegistry {
tools: HashMap<String, Box<dyn Tool>>,
}
impl ToolRegistry {
#[must_use]
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn register(&mut self, tool: impl Tool + 'static) {
let name = tool.name().to_string();
if self.tools.contains_key(&name) {
tracing::warn!(
tool = %name,
"overwriting previously registered tool with the same name"
);
}
self.tools.insert(name, Box::new(tool));
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&dyn Tool> {
self.tools.get(name).map(std::convert::AsRef::as_ref)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
#[must_use]
pub fn all_schemas(&self) -> Vec<ToolSchema> {
self.tools.values().map(|t| t.schema()).collect()
}
#[must_use]
pub fn tool_names(&self) -> Vec<String> {
let mut names: Vec<_> = self.tools.keys().cloned().collect();
names.sort();
names
}
#[must_use]
pub fn len(&self) -> usize {
self.tools.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
#[must_use]
pub fn concurrent_safe_tools(&self) -> Vec<&dyn Tool> {
self.tools
.values()
.map(std::convert::AsRef::as_ref)
.filter(|t| t.is_concurrency_safe())
.collect()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
pub type ToolFn =
fn(
Value,
&ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + 'static>>;
pub type ConcurrencyCheckFn = fn(&Value) -> bool;
pub struct FnTool {
pub name: String,
pub description: String,
pub input_schema: Value,
pub tool_fn: ToolFn,
pub is_concurrency_safe: bool,
pub concurrency_check_fn: Option<ConcurrencyCheckFn>,
pub is_read_only: bool,
pub system_prompt: Option<String>,
}
impl FnTool {
pub fn new(name: String, description: String, input_schema: Value, tool_fn: ToolFn) -> Self {
Self {
name,
description,
input_schema,
tool_fn,
is_concurrency_safe: false,
concurrency_check_fn: None,
is_read_only: false,
system_prompt: None,
}
}
#[must_use]
pub fn concurrency_safe(mut self) -> Self {
self.is_concurrency_safe = true;
self
}
#[must_use]
pub fn with_concurrency_check(mut self, check_fn: ConcurrencyCheckFn) -> Self {
self.concurrency_check_fn = Some(check_fn);
self
}
#[must_use]
pub fn read_only(mut self) -> Self {
self.is_read_only = true;
self
}
#[must_use]
pub fn with_system_prompt(mut self, prompt: String) -> Self {
self.system_prompt = Some(prompt);
self
}
}
impl Tool for FnTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn schema(&self) -> ToolSchema {
ToolSchema {
tool: self.name.clone(),
description: self.description.clone(),
input_schema: self.input_schema.clone(),
}
}
fn call(
&self,
input: Value,
context: &ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + '_>> {
(self.tool_fn)(input, context)
}
fn is_concurrency_safe(&self) -> bool {
self.is_concurrency_safe
}
fn is_safe_for_concurrent_execution(&self, input: &Value) -> bool {
self.concurrency_check_fn
.map_or(self.is_concurrency_safe, |f| f(input))
}
fn is_read_only(&self) -> bool {
self.is_read_only
}
fn system_prompt(&self) -> Option<String> {
self.system_prompt.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_tool_fn(
_input: Value,
_ctx: &ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + 'static>> {
Box::pin(async { Ok(ToolOutput::text("ok")) })
}
fn make_tool(name: &str) -> FnTool {
FnTool::new(
name.into(),
"test tool".into(),
serde_json::json!({"type": "object"}),
test_tool_fn,
)
}
#[test]
fn register_duplicate_overwrites_previous() {
let mut registry = ToolRegistry::new();
registry.register(make_tool("my_tool"));
assert_eq!(registry.len(), 1);
registry.register(make_tool("my_tool"));
assert_eq!(
registry.len(),
1,
"duplicate registration should not increase count"
);
}
#[test]
fn register_different_names_adds_both() {
let mut registry = ToolRegistry::new();
registry.register(make_tool("tool_a"));
registry.register(make_tool("tool_b"));
assert_eq!(registry.len(), 2);
}
#[test]
fn register_overwrite_uses_new_tool() {
let mut registry = ToolRegistry::new();
registry.register(make_tool("my_tool"));
registry.register(make_tool("my_tool"));
assert!(registry.get("my_tool").is_some());
}
}