lc_core/tools/base.rs
1// src/core/tools/base.rs
2//! Tool base traits.
3//!
4//! Python's BaseTool uses a simplified run(input: str) -> str interface.
5
6use crate::runnables::{LcelError, Runnable, RunnableConfig};
7use async_trait::async_trait;
8use schemars::JsonSchema;
9use serde::{de::DeserializeOwned, Serialize};
10use serde_json::Value;
11use std::sync::Arc;
12
13/// Base tool trait (object-safe version).
14///
15/// This is the base interface for tool registries and Agents.
16/// Uses string input/output to simplify LLM calls.
17///
18/// All tools must implement this interface to be used by Agents.
19#[async_trait]
20pub trait BaseTool: Send + Sync {
21 /// Returns the tool name.
22 ///
23 /// Name should be unique and clearly express the tool's purpose.
24 fn name(&self) -> &str;
25
26 /// Returns the tool description.
27 ///
28 /// Description should detail the tool's purpose, input format, and output format.
29 fn description(&self) -> &str;
30
31 /// Execute the tool (string version).
32 ///
33 /// This is the primary interface called by Agents.
34 /// Input is typically a JSON string, output is the execution result.
35 ///
36 /// # Arguments
37 /// * `input` - Tool input (typically JSON-formatted string).
38 ///
39 /// # Returns
40 /// String representation of execution result.
41 async fn run(&self, input: String) -> Result<String, ToolError>;
42
43 /// Returns the input JSON Schema.
44 ///
45 /// Used to describe the tool's input format to the LLM.
46 fn args_schema(&self) -> Option<Value> {
47 None
48 }
49
50 /// Whether to return result directly to user.
51 ///
52 /// If true, tool output is returned directly to user, not passed to Agent.
53 fn return_direct(&self) -> bool {
54 false
55 }
56
57 /// Handle execution error.
58 ///
59 /// Returns a friendly error message when tool execution fails.
60 async fn handle_error(&self, error: ToolError) -> String {
61 format!("Tool '{}' execution failed: {}", self.name(), error)
62 }
63}
64
65/// Generic tool trait (type-safe version).
66///
67/// For scenarios requiring type-safe input/output.
68/// Tools implementing this trait can be automatically wrapped as BaseTool.
69#[async_trait]
70pub trait Tool: Send + Sync {
71 /// Input type (must support deserialization and JSON Schema).
72 type Input: DeserializeOwned + JsonSchema + Send + Sync + 'static;
73
74 /// Output type (must support serialization).
75 type Output: Serialize + Send + Sync;
76
77 /// Execute the tool.
78 ///
79 /// # Arguments
80 /// * `input` - Tool input.
81 ///
82 /// # Returns
83 /// Tool output.
84 async fn invoke(&self, input: Self::Input) -> Result<Self::Output, ToolError>;
85
86 /// Returns the input JSON Schema.
87 fn args_schema(&self) -> Option<Value> {
88 use schemars::schema_for;
89 serde_json::to_value(schema_for!(Self::Input)).ok()
90 }
91}
92
93/// Tool error type.
94#[derive(Debug, thiserror::Error)]
95#[non_exhaustive]
96pub enum ToolError {
97 /// Input validation error.
98 #[error("Invalid input: {0}")]
99 InvalidInput(String),
100
101 /// Execution error.
102 #[error("Execution failed: {0}")]
103 ExecutionFailed(String),
104
105 /// Timeout.
106 #[error("Timeout: {0} seconds")]
107 Timeout(u64),
108
109 /// Tool not found.
110 #[error("Tool not found: {0}")]
111 ToolNotFound(String),
112
113 /// MCP transport-layer error (adapted via `MCPToolAdapter`), preserving code/message/data (P1-6).
114 ///
115 /// Not silently downgraded to `ExecutionFailed`: the caller can distinguish connection drop /
116 /// method-not-found / argument errors by `code`.
117 #[error("MCP error [{code}]: {message}")]
118 McpError {
119 /// MCP error code
120 code: i32,
121 /// MCP error message
122 message: String,
123 /// Additional error data (optional)
124 data: Option<Value>,
125 },
126}
127
128use super::ToolDefinition;
129
130/// Converts BaseTool to ToolDefinition (for function calling).
131///
132/// # Arguments
133/// * `tool` - Tool implementing BaseTool trait.
134///
135/// # Returns
136/// ToolDefinition for bind_tools().
137///
138/// # Example
139/// ```ignore
140/// use langchainrust::{Calculator, BaseTool, to_tool_definition};
141/// use std::sync::Arc;
142///
143/// let calculator = Calculator::new();
144/// let tool_def = to_tool_definition(&calculator);
145/// ```
146pub fn to_tool_definition(tool: &dyn BaseTool) -> ToolDefinition {
147 ToolDefinition::new(tool.name(), tool.description()).with_parameters(
148 tool.args_schema()
149 .unwrap_or(serde_json::json!({"type": "object"})),
150 )
151}
152
153// Runnable form: lets a tool enter an LCEL chain, so `tool.pipe(...)` works.
154// Receives a String (usually JSON input), delegates to `run`; errors go into `LcelError::Tool` via `From<ToolError>`.
155#[async_trait]
156impl Runnable<String, String> for Arc<dyn BaseTool> {
157 type Error = LcelError;
158
159 async fn invoke(
160 &self,
161 input: String,
162 _config: Option<RunnableConfig>,
163 ) -> Result<String, LcelError> {
164 BaseTool::run(&**self, input).await.map_err(LcelError::from)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use std::sync::Arc;
172
173 /// Simple echo tool: returns `echo: {input}`.
174 struct EchoTool;
175
176 #[async_trait]
177 impl BaseTool for EchoTool {
178 fn name(&self) -> &str {
179 "echo"
180 }
181 fn description(&self) -> &str {
182 "回显输入"
183 }
184 async fn run(&self, input: String) -> Result<String, ToolError> {
185 Ok(format!("echo: {input}"))
186 }
187 }
188
189 #[tokio::test]
190 async fn arc_tool_is_runnable() {
191 let tool: Arc<dyn BaseTool> = Arc::new(EchoTool);
192 let result = tool.invoke("hi".to_string(), None).await.unwrap();
193 assert_eq!(result, "echo: hi");
194 }
195
196 #[tokio::test]
197 async fn arc_tool_pipes() {
198 use crate::runnables::{RunnableExt, RunnableLambda};
199
200 let tool: Arc<dyn BaseTool> = Arc::new(EchoTool);
201 let chain = tool.pipe(RunnableLambda::new_sync(|s: String| s.to_uppercase()));
202 let result = chain.invoke("hi".to_string(), None).await.unwrap();
203 assert_eq!(result, "ECHO: HI");
204 }
205
206 #[tokio::test]
207 async fn arc_tool_error_maps_to_lcel() {
208 struct FailingTool;
209 #[async_trait]
210 impl BaseTool for FailingTool {
211 fn name(&self) -> &str {
212 "fail"
213 }
214 fn description(&self) -> &str {
215 "总是失败"
216 }
217 async fn run(&self, _input: String) -> Result<String, ToolError> {
218 Err(ToolError::ExecutionFailed("boom".to_string()))
219 }
220 }
221
222 let tool: Arc<dyn BaseTool> = Arc::new(FailingTool);
223 let err = tool.invoke("x".to_string(), None).await.unwrap_err();
224 assert!(matches!(err, LcelError::Tool(ref msg) if msg.contains("boom")));
225 }
226}