Skip to main content

lc_shared/
tool_types.rs

1// lc-shared/src/tool_types.rs
2//! Tool call types shared across crates.
3//!
4//! These types are needed by both `lc-schema` (Message uses ToolCall)
5//! and `lc-core` (tool definitions), so they live here to break the
6//! circular dependency between schema and core.
7
8use crate::json_repair::{parse_tolerant_json, JsonRepairError};
9use serde::{de::DeserializeOwned, Deserialize, Serialize};
10
11/// Tool call from LLM response
12///
13/// When an LLM decides to call a tool, it returns a ToolCall structure.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ToolCall {
16    /// Tool call ID (used to reference the call result)
17    pub id: String,
18
19    /// Tool type (always "function")
20    #[serde(rename = "type")]
21    pub tool_type: String,
22
23    /// Function call details
24    pub function: FunctionCall,
25}
26
27impl ToolCall {
28    /// Create a new tool call.
29    ///
30    /// The three positional arguments are easy to swap (id/name/arguments);
31    /// prefer [`ToolCall::builder`] for call sites that construct calls from
32    /// untrusted or variable input.
33    #[deprecated(note = "use ToolCall::builder(id).name(..).arguments(..).build() instead")]
34    pub fn new(
35        id: impl Into<String>,
36        name: impl Into<String>,
37        arguments: impl Into<String>,
38    ) -> Self {
39        Self {
40            id: id.into(),
41            tool_type: "function".to_string(),
42            function: FunctionCall {
43                name: name.into(),
44                arguments: arguments.into(),
45            },
46        }
47    }
48
49    /// Create a builder for a [`ToolCall`].
50    pub fn builder(id: impl Into<String>) -> ToolCallBuilder {
51        ToolCallBuilder::new(id)
52    }
53
54    /// Get the function name
55    pub fn name(&self) -> &str {
56        &self.function.name
57    }
58
59    /// Get the arguments as string
60    pub fn arguments(&self) -> &str {
61        &self.function.arguments
62    }
63
64    /// Parse arguments as JSON.
65    ///
66    /// Arguments come from LLM output, so a tolerant parser is used — code
67    /// fences, trailing commas, unescaped quotes and trailing garbage are
68    /// repaired before deserialization (see [`crate::json_repair`]).
69    pub fn parse_arguments<T: DeserializeOwned>(&self) -> Result<T, JsonRepairError> {
70        parse_tolerant_json(&self.function.arguments)
71    }
72}
73
74/// Builder for constructing a [`ToolCall`] field by field.
75///
76/// Replaces the error-prone 3-positional-argument [`ToolCall::new`].
77///
78/// ```
79/// use lc_shared::ToolCall;
80///
81/// let call = ToolCall::builder("call_1")
82///     .name("get_weather")
83///     .arguments(r#"{"city":"beijing"}"#)
84///     .build();
85///
86/// assert_eq!(call.id, "call_1");
87/// assert_eq!(call.name(), "get_weather");
88/// ```
89#[derive(Debug, Clone)]
90pub struct ToolCallBuilder {
91    id: String,
92    tool_type: String,
93    function: FunctionCall,
94}
95
96impl ToolCallBuilder {
97    /// Start building a tool call with its id.
98    pub fn new(id: impl Into<String>) -> Self {
99        Self {
100            id: id.into(),
101            tool_type: "function".to_string(),
102            function: FunctionCall {
103                name: String::new(),
104                arguments: String::new(),
105            },
106        }
107    }
108
109    /// Set the function name.
110    pub fn name(mut self, name: impl Into<String>) -> Self {
111        self.function.name = name.into();
112        self
113    }
114
115    /// Set the JSON-encoded function arguments.
116    pub fn arguments(mut self, arguments: impl Into<String>) -> Self {
117        self.function.arguments = arguments.into();
118        self
119    }
120
121    /// Consume the builder and produce the [`ToolCall`].
122    pub fn build(self) -> ToolCall {
123        ToolCall {
124            id: self.id,
125            tool_type: self.tool_type,
126            function: self.function,
127        }
128    }
129}
130
131/// Function call inside a ToolCall
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct FunctionCall {
134    /// Function name
135    pub name: String,
136
137    /// Arguments as JSON string
138    pub arguments: String,
139}
140
141/// Tool call result to send back to LLM
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ToolCallResult {
144    /// Tool call ID (must match the ToolCall.id)
145    pub tool_call_id: String,
146
147    /// Role (always "tool")
148    pub role: String,
149
150    /// Tool output content
151    pub content: String,
152}
153
154impl ToolCallResult {
155    /// Create a new tool result
156    pub fn new(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
157        Self {
158            tool_call_id: tool_call_id.into(),
159            role: "tool".to_string(),
160            content: content.into(),
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use serde_json::json;
169    use std::collections::HashMap;
170
171    #[test]
172    fn test_tool_call() {
173        let call = ToolCall::builder("call_123")
174            .name("calculator")
175            .arguments(json!({"expression": "2 + 3"}).to_string())
176            .build();
177
178        assert_eq!(call.id, "call_123");
179        assert_eq!(call.name(), "calculator");
180
181        let args: HashMap<String, String> = call.parse_arguments().unwrap();
182        assert_eq!(args.get("expression").unwrap(), "2 + 3");
183    }
184
185    #[test]
186    fn test_parse_arguments_tolerates_messy_llm_json() {
187        // LLM-generated arguments with trailing comma + trailing garbage
188        let call = ToolCall::builder("call_456")
189            .name("weather")
190            .arguments(r#"{"city": "beijing", "unit": "celsius",} plus extra text"#)
191            .build();
192
193        let args: HashMap<String, String> = call.parse_arguments().unwrap();
194        assert_eq!(args.get("city").unwrap(), "beijing");
195        assert_eq!(args.get("unit").unwrap(), "celsius");
196    }
197
198    #[test]
199    fn test_tool_call_result() {
200        let result = ToolCallResult::new("call_123", "5");
201
202        assert_eq!(result.tool_call_id, "call_123");
203        assert_eq!(result.role, "tool");
204        assert_eq!(result.content, "5");
205    }
206}