use comp_cat_rs::effect::io::Io;
use serde_json::Value;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct ToolDefinition {
name: String,
description: String,
parameters_schema: Value,
}
impl ToolDefinition {
#[must_use]
pub fn new(name: String, description: String, parameters_schema: Value) -> Self {
Self { name, description, parameters_schema }
}
#[must_use]
pub fn name(&self) -> &str { &self.name }
#[must_use]
pub fn description(&self) -> &str { &self.description }
#[must_use]
pub fn parameters_schema(&self) -> &Value { &self.parameters_schema }
#[must_use]
pub fn to_api_json(&self) -> Value {
serde_json::json!({
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters_schema
}
})
}
}
pub trait Tool {
fn definition(&self) -> ToolDefinition;
fn call(&self, args: Value) -> Io<Error, Value>;
}
pub struct Toolbox<T: Tool> {
tools: Vec<T>,
}
impl<T: Tool> Toolbox<T> {
#[must_use]
pub fn new() -> Self { Self { tools: Vec::new() } }
#[must_use]
pub fn with_tool(self, tool: T) -> Self {
Self {
tools: self.tools.into_iter().chain(std::iter::once(tool)).collect(),
}
}
#[must_use]
pub fn invoke(&self, name: &str, args: Value) -> Io<Error, Value> {
let name_owned = name.to_owned();
self.tools.iter()
.find(|t| t.definition().name() == name_owned)
.map_or_else(
move || Io::suspend(move || {
Err(Error::Config { field: format!("unknown tool: {name_owned}") })
}),
|t| t.call(args),
)
}
#[must_use]
pub fn definitions(&self) -> Vec<Value> {
self.tools.iter().map(|t| t.definition().to_api_json()).collect()
}
}
impl<T: Tool> Default for Toolbox<T> {
fn default() -> Self { Self::new() }
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeTool {
name: String,
}
impl Tool for FakeTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition::new(
self.name.clone(),
"a fake tool".into(),
serde_json::json!({"type": "object"}),
)
}
fn call(&self, _args: Value) -> Io<Error, Value> {
Io::pure(serde_json::json!({"result": "ok"}))
}
}
#[test]
fn invoke_known_tool_succeeds() -> Result<(), Error> {
let toolbox = Toolbox::new()
.with_tool(FakeTool { name: "greet".into() });
let result = toolbox.invoke("greet", serde_json::json!({})).run()?;
assert_eq!(result, serde_json::json!({"result": "ok"}));
Ok(())
}
#[test]
fn invoke_unknown_tool_returns_error() {
let toolbox: Toolbox<FakeTool> = Toolbox::new();
let result = toolbox.invoke("nonexistent", serde_json::json!({})).run();
assert!(result.is_err());
}
#[test]
fn definitions_lists_all_tools() {
let toolbox = Toolbox::new()
.with_tool(FakeTool { name: "a".into() })
.with_tool(FakeTool { name: "b".into() });
let defs = toolbox.definitions();
assert_eq!(defs.len(), 2);
}
}