pub mod web_search;
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
use crate::{Result, RoutexError};
#[derive(Debug, Clone)]
pub struct Parameter {
pub kind: String,
pub description: String,
pub required: bool,
}
#[derive(Debug, Clone)]
pub struct Schema {
pub description: String,
pub parameters: HashMap<String, Parameter>,
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn schema(&self) -> Schema;
async fn execute(&self, input: Value) -> Result<Value>;
}
#[derive(Debug, Clone)]
pub struct ToolInfo {
pub name: String,
pub description: String,
}
pub struct Registry {
tools: HashMap<String, Arc<dyn Tool>>,
}
impl Registry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn register(&mut self, tool: impl Tool + 'static) {
let name = tool.name().to_string();
self.tools.insert(name, Arc::new(tool));
}
pub async fn execute(&self, name: &str, input: Value) -> Result<Value> {
let tool = self
.tools
.get(name)
.ok_or_else(|| RoutexError::ToolNotFound {
name: name.to_string(),
})?;
tool.execute(input)
.await
.map_err(|e| RoutexError::ToolFailed {
name: name.to_string(),
reason: e.to_string(),
})
}
pub fn has(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn list(&self) -> Vec<ToolInfo> {
let mut infos: Vec<ToolInfo> = self
.tools
.values()
.map(|t| ToolInfo {
name: t.name().to_string(),
description: t.schema().description.clone(),
})
.collect();
infos.sort_by(|a, b| a.name.cmp(&b.name));
infos
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
self.tools.get(name).cloned()
}
pub fn len(&self) -> usize {
self.tools.len()
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct RoutexTool;
#[async_trait]
impl Tool for RoutexTool {
fn name(&self) -> &str {
"routex"
}
fn schema(&self) -> Schema {
Schema {
description: "Routex the yaml file".to_string(),
parameters: HashMap::from([(
"message".to_string(),
Parameter {
kind: "string".to_string(),
description: "Message to routex".to_string(),
required: true,
},
)]),
}
}
async fn execute(&self, input: Value) -> Result<Value> {
Ok(input)
}
}
#[test]
fn test_register_and_has() {
let mut registry = Registry::new();
registry.register(RoutexTool);
assert!(registry.has("routex"));
assert!(!registry.has("nonexistent"));
}
#[test]
fn test_list_returns_sorted() {
let mut registry = Registry::new();
registry.register(RoutexTool);
let list = registry.list();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name, "routex");
}
#[tokio::test]
async fn test_execute_known_tool() {
let mut registry = Registry::new();
registry.register(RoutexTool);
let result = registry
.execute("routex", json!({"message": "hello"}))
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), json!({"message": "hello"}))
}
#[tokio::test]
async fn test_execute_unknown_tool() {
let mut registry = Registry::default();
registry.register(RoutexTool);
let result = registry.execute("nonexistent", json!({})).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("nonexistent"))
}
#[test]
fn test_len_and_is_empty() {
let mut registry = Registry::new();
assert!(registry.is_empty());
registry.register(RoutexTool);
assert_eq!(registry.len(), 1);
assert!(!registry.is_empty());
}
}