use pe_core::error::PeError;
use pe_core::llm::ToolSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn schema(&self) -> ToolSchema;
fn execute(&self, input: Value) -> ToolFuture;
fn execute_structured(&self, input: Value) -> ToolResultFuture {
let fut = self.execute(input);
Box::pin(async move {
let output = fut.await?;
Ok(ToolResult::ok(output))
})
}
}
pub type ToolFuture = Pin<Box<dyn Future<Output = Result<Value, PeError>> + Send>>;
pub type ToolResultFuture = Pin<Box<dyn Future<Output = Result<ToolResult, PeError>> + Send>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub output: Value,
pub success: bool,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
impl ToolResult {
pub fn ok(output: Value) -> Self {
Self {
output,
success: true,
metadata: HashMap::new(),
}
}
pub fn error(msg: impl Into<String>) -> Self {
Self {
output: Value::String(msg.into()),
success: false,
metadata: HashMap::new(),
}
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
impl From<Value> for ToolResult {
fn from(v: Value) -> Self {
Self::ok(v)
}
}
pub type ToolFunc = Arc<
dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, PeError>> + Send>> + Send + Sync,
>;
pub struct FunctionTool {
name: String,
description: String,
schema: ToolSchema,
func: ToolFunc,
}
impl FunctionTool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
func: impl Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, PeError>> + Send>>
+ Send
+ Sync
+ 'static,
) -> Self {
let name = name.into();
let description = description.into();
Self {
schema: ToolSchema {
name: name.clone(),
description: description.clone(),
parameters,
strict: false,
},
name,
description,
func: Arc::new(func),
}
}
}
impl Tool for FunctionTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn schema(&self) -> ToolSchema {
self.schema.clone()
}
fn execute(&self, input: Value) -> ToolFuture {
(self.func)(input)
}
}
pub type StructuredToolFunc = Arc<
dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<ToolResult, PeError>> + Send>>
+ Send
+ Sync,
>;
pub struct StructuredFunctionTool {
name: String,
description: String,
schema: ToolSchema,
func: StructuredToolFunc,
}
impl StructuredFunctionTool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
func: impl Fn(Value) -> Pin<Box<dyn Future<Output = Result<ToolResult, PeError>> + Send>>
+ Send
+ Sync
+ 'static,
) -> Self {
let name = name.into();
let description = description.into();
Self {
schema: ToolSchema {
name: name.clone(),
description: description.clone(),
parameters,
strict: false,
},
name,
description,
func: Arc::new(func),
}
}
}
impl Tool for StructuredFunctionTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn schema(&self) -> ToolSchema {
self.schema.clone()
}
fn execute(&self, input: Value) -> ToolFuture {
let func = self.func.clone();
Box::pin(async move {
let result = func(input).await?;
Ok(result.output)
})
}
fn execute_structured(&self, input: Value) -> ToolResultFuture {
(self.func)(input)
}
}
impl std::fmt::Debug for StructuredFunctionTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StructuredFunctionTool")
.field("name", &self.name)
.field("description", &self.description)
.finish()
}
}
impl std::fmt::Debug for FunctionTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FunctionTool")
.field("name", &self.name)
.field("description", &self.description)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn function_tool_executes_correctly() {
let tool = FunctionTool::new(
"add",
"Add two numbers",
serde_json::json!({
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}),
|input| {
Box::pin(async move {
let a = input["a"].as_f64().unwrap_or(0.0);
let b = input["b"].as_f64().unwrap_or(0.0);
Ok(serde_json::json!(a + b))
})
},
);
assert_eq!(tool.name(), "add");
assert_eq!(tool.description(), "Add two numbers");
let schema = tool.schema();
assert_eq!(schema.name, "add");
assert!(!schema.strict);
let result = tool
.execute(serde_json::json!({"a": 3, "b": 4}))
.await
.unwrap();
assert_eq!(result, serde_json::json!(7.0));
}
#[test]
fn test_tool_result_ok() {
let result = ToolResult::ok(serde_json::json!({"answer": 42}));
assert!(result.success);
assert_eq!(result.output, serde_json::json!({"answer": 42}));
assert!(result.metadata.is_empty());
}
#[test]
fn test_tool_result_error() {
let result = ToolResult::error("something went wrong");
assert!(!result.success);
assert_eq!(
result.output,
serde_json::Value::String("something went wrong".into())
);
assert!(result.metadata.is_empty());
}
#[test]
fn test_tool_result_metadata() {
let result = ToolResult::ok(serde_json::json!({"data": [1, 2, 3]}))
.with_metadata("source", serde_json::json!("database"))
.with_metadata("result_count", serde_json::json!(3))
.with_metadata("confidence", serde_json::json!(0.95));
assert!(result.success);
assert_eq!(result.metadata.len(), 3);
assert_eq!(result.metadata["source"], serde_json::json!("database"));
assert_eq!(result.metadata["result_count"], serde_json::json!(3));
assert_eq!(result.metadata["confidence"], serde_json::json!(0.95));
}
#[test]
fn test_tool_result_from_value() {
let value = serde_json::json!({"key": "val"});
let result: ToolResult = value.clone().into();
assert!(result.success);
assert_eq!(result.output, value);
assert!(result.metadata.is_empty());
}
#[tokio::test]
async fn test_execute_structured_default() {
let tool = FunctionTool::new(
"add",
"Add two numbers",
serde_json::json!({"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}}),
|input| {
Box::pin(async move {
let a = input["a"].as_f64().unwrap_or(0.0);
let b = input["b"].as_f64().unwrap_or(0.0);
Ok(serde_json::json!(a + b))
})
},
);
let result = tool
.execute_structured(serde_json::json!({"a": 3, "b": 4}))
.await
.unwrap();
assert!(result.success);
assert_eq!(result.output, serde_json::json!(7.0));
assert!(result.metadata.is_empty());
}
#[tokio::test]
async fn function_tool_propagates_error() {
let tool = FunctionTool::new(
"fail",
"Always fails",
serde_json::json!({"type": "object"}),
|_input| {
Box::pin(async move {
Err(PeError::ToolExecution {
tool: "fail".into(),
reason: "intentional failure".into(),
})
})
},
);
let result = tool.execute(serde_json::json!({})).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("intentional failure"));
}
}