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, PartialEq)]
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 builder for a [`ToolCall`].
29    pub fn builder(id: impl Into<String>) -> ToolCallBuilder {
30        ToolCallBuilder::new(id)
31    }
32
33    /// Get the function name
34    pub fn name(&self) -> &str {
35        &self.function.name
36    }
37
38    /// Get the arguments as string
39    pub fn arguments(&self) -> &str {
40        &self.function.arguments
41    }
42
43    /// Parse arguments as JSON.
44    ///
45    /// Arguments come from LLM output, so a tolerant parser is used — code
46    /// fences, trailing commas, unescaped quotes and trailing garbage are
47    /// repaired before deserialization (see [`crate::json_repair`]).
48    pub fn parse_arguments<T: DeserializeOwned>(&self) -> Result<T, JsonRepairError> {
49        parse_tolerant_json(&self.function.arguments)
50    }
51}
52
53/// Builder for constructing a [`ToolCall`] field by field.
54///
55/// Replaces the error-prone 3-positional-argument constructor `ToolCall::new`
56/// (removed in 0.17; use [`ToolCall::builder`] instead).
57///
58/// ```
59/// use lc_shared::ToolCall;
60///
61/// let call = ToolCall::builder("call_1")
62///     .name("get_weather")
63///     .arguments(r#"{"city":"beijing"}"#)
64///     .build();
65///
66/// assert_eq!(call.id, "call_1");
67/// assert_eq!(call.name(), "get_weather");
68/// ```
69#[derive(Debug, Clone)]
70pub struct ToolCallBuilder {
71    id: String,
72    tool_type: String,
73    function: FunctionCall,
74}
75
76impl ToolCallBuilder {
77    /// Start building a tool call with its id.
78    pub fn new(id: impl Into<String>) -> Self {
79        Self {
80            id: id.into(),
81            tool_type: "function".to_string(),
82            function: FunctionCall {
83                name: String::new(),
84                arguments: String::new(),
85            },
86        }
87    }
88
89    /// Set the function name.
90    pub fn name(mut self, name: impl Into<String>) -> Self {
91        self.function.name = name.into();
92        self
93    }
94
95    /// Set the JSON-encoded function arguments.
96    pub fn arguments(mut self, arguments: impl Into<String>) -> Self {
97        self.function.arguments = arguments.into();
98        self
99    }
100
101    /// Consume the builder and produce the [`ToolCall`].
102    pub fn build(self) -> ToolCall {
103        ToolCall {
104            id: self.id,
105            tool_type: self.tool_type,
106            function: self.function,
107        }
108    }
109}
110
111/// Function call inside a ToolCall
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113pub struct FunctionCall {
114    /// Function name
115    pub name: String,
116
117    /// Arguments as JSON string
118    pub arguments: String,
119}
120
121/// Tool call result to send back to LLM
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ToolCallResult {
124    /// Tool call ID (must match the ToolCall.id)
125    pub tool_call_id: String,
126
127    /// Role (always "tool")
128    pub role: String,
129
130    /// Tool output content
131    pub content: String,
132}
133
134impl ToolCallResult {
135    /// Create a new tool result
136    pub fn new(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
137        Self {
138            tool_call_id: tool_call_id.into(),
139            role: "tool".to_string(),
140            content: content.into(),
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use serde_json::json;
149    use std::collections::HashMap;
150
151    #[test]
152    fn test_tool_call() {
153        let call = ToolCall::builder("call_123")
154            .name("calculator")
155            .arguments(json!({"expression": "2 + 3"}).to_string())
156            .build();
157
158        assert_eq!(call.id, "call_123");
159        assert_eq!(call.name(), "calculator");
160
161        let args: HashMap<String, String> = call.parse_arguments().unwrap();
162        assert_eq!(args.get("expression").unwrap(), "2 + 3");
163    }
164
165    #[test]
166    fn test_parse_arguments_tolerates_messy_llm_json() {
167        // LLM-generated arguments with trailing comma + trailing garbage
168        let call = ToolCall::builder("call_456")
169            .name("weather")
170            .arguments(r#"{"city": "beijing", "unit": "celsius",} plus extra text"#)
171            .build();
172
173        let args: HashMap<String, String> = call.parse_arguments().unwrap();
174        assert_eq!(args.get("city").unwrap(), "beijing");
175        assert_eq!(args.get("unit").unwrap(), "celsius");
176    }
177
178    #[test]
179    fn test_tool_call_result() {
180        let result = ToolCallResult::new("call_123", "5");
181
182        assert_eq!(result.tool_call_id, "call_123");
183        assert_eq!(result.role, "tool");
184        assert_eq!(result.content, "5");
185    }
186}