Skip to main content

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)]
95pub enum ToolError {
96    /// Input validation error.
97    #[error("Invalid input: {0}")]
98    InvalidInput(String),
99
100    /// Execution error.
101    #[error("Execution failed: {0}")]
102    ExecutionFailed(String),
103
104    /// Timeout.
105    #[error("Timeout: {0} seconds")]
106    Timeout(u64),
107
108    /// Tool not found.
109    #[error("Tool not found: {0}")]
110    ToolNotFound(String),
111
112    /// MCP 传输层错误(经 `MCPToolAdapter` 适配),保留 code/message/data(P1-6)。
113    ///
114    /// 不静默降级为 `ExecutionFailed`,上层可据 `code` 区分连接断开 / 方法不存在 /
115    /// 参数错误等场景。
116    #[error("MCP error [{code}]: {message}")]
117    McpError {
118        code: i32,
119        message: String,
120        data: Option<Value>,
121    },
122}
123
124use super::ToolDefinition;
125
126/// Converts BaseTool to ToolDefinition (for function calling).
127///
128/// # Arguments
129/// * `tool` - Tool implementing BaseTool trait.
130///
131/// # Returns
132/// ToolDefinition for bind_tools().
133///
134/// # Example
135/// ```ignore
136/// use langchainrust::{Calculator, BaseTool, to_tool_definition};
137/// use std::sync::Arc;
138///
139/// let calculator = Calculator::new();
140/// let tool_def = to_tool_definition(&calculator);
141/// ```
142pub fn to_tool_definition(tool: &dyn BaseTool) -> ToolDefinition {
143    ToolDefinition::new(tool.name(), tool.description()).with_parameters(
144        tool.args_schema()
145            .unwrap_or(serde_json::json!({"type": "object"})),
146    )
147}
148
149// Runnable 形态:让工具能进 LCEL 链,`tool.pipe(...)` 成立。
150// 接收 String(通常是 JSON 输入),委托给 `run`,错误经 `From<ToolError>` 进 `LcelError::Tool`。
151#[async_trait]
152impl Runnable<String, String> for Arc<dyn BaseTool> {
153    type Error = LcelError;
154
155    async fn invoke(
156        &self,
157        input: String,
158        _config: Option<RunnableConfig>,
159    ) -> Result<String, LcelError> {
160        BaseTool::run(&**self, input).await.map_err(LcelError::from)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::sync::Arc;
168
169    /// 简单回显工具:返回 `echo: {input}`。
170    struct EchoTool;
171
172    #[async_trait]
173    impl BaseTool for EchoTool {
174        fn name(&self) -> &str {
175            "echo"
176        }
177        fn description(&self) -> &str {
178            "回显输入"
179        }
180        async fn run(&self, input: String) -> Result<String, ToolError> {
181            Ok(format!("echo: {input}"))
182        }
183    }
184
185    #[tokio::test]
186    async fn arc_tool_is_runnable() {
187        let tool: Arc<dyn BaseTool> = Arc::new(EchoTool);
188        let result = tool.invoke("hi".to_string(), None).await.unwrap();
189        assert_eq!(result, "echo: hi");
190    }
191
192    #[tokio::test]
193    async fn arc_tool_pipes() {
194        use crate::runnables::{RunnableExt, RunnableLambda};
195
196        let tool: Arc<dyn BaseTool> = Arc::new(EchoTool);
197        let chain = tool.pipe(RunnableLambda::new_sync(|s: String| s.to_uppercase()));
198        let result = chain.invoke("hi".to_string(), None).await.unwrap();
199        assert_eq!(result, "ECHO: HI");
200    }
201
202    #[tokio::test]
203    async fn arc_tool_error_maps_to_lcel() {
204        struct FailingTool;
205        #[async_trait]
206        impl BaseTool for FailingTool {
207            fn name(&self) -> &str {
208                "fail"
209            }
210            fn description(&self) -> &str {
211                "总是失败"
212            }
213            async fn run(&self, _input: String) -> Result<String, ToolError> {
214                Err(ToolError::ExecutionFailed("boom".to_string()))
215            }
216        }
217
218        let tool: Arc<dyn BaseTool> = Arc::new(FailingTool);
219        let err = tool.invoke("x".to_string(), None).await.unwrap_err();
220        assert!(matches!(err, LcelError::Tool(ref msg) if msg.contains("boom")));
221    }
222}