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    pub fn new(
30        id: impl Into<String>,
31        name: impl Into<String>,
32        arguments: impl Into<String>,
33    ) -> Self {
34        Self {
35            id: id.into(),
36            tool_type: "function".to_string(),
37            function: FunctionCall {
38                name: name.into(),
39                arguments: arguments.into(),
40            },
41        }
42    }
43
44    /// Get the function name
45    pub fn name(&self) -> &str {
46        &self.function.name
47    }
48
49    /// Get the arguments as string
50    pub fn arguments(&self) -> &str {
51        &self.function.arguments
52    }
53
54    /// Parse arguments as JSON.
55    ///
56    /// Arguments come from LLM output, so a tolerant parser is used — code
57    /// fences, trailing commas, unescaped quotes and trailing garbage are
58    /// repaired before deserialization (see [`crate::json_repair`]).
59    pub fn parse_arguments<T: DeserializeOwned>(&self) -> Result<T, JsonRepairError> {
60        parse_tolerant_json(&self.function.arguments)
61    }
62}
63
64/// Function call inside a ToolCall
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct FunctionCall {
67    /// Function name
68    pub name: String,
69
70    /// Arguments as JSON string
71    pub arguments: String,
72}
73
74/// Tool call result to send back to LLM
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ToolCallResult {
77    /// Tool call ID (must match the ToolCall.id)
78    pub tool_call_id: String,
79
80    /// Role (always "tool")
81    pub role: String,
82
83    /// Tool output content
84    pub content: String,
85}
86
87impl ToolCallResult {
88    /// Create a new tool result
89    pub fn new(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
90        Self {
91            tool_call_id: tool_call_id.into(),
92            role: "tool".to_string(),
93            content: content.into(),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use serde_json::json;
102    use std::collections::HashMap;
103
104    #[test]
105    fn test_tool_call() {
106        let call = ToolCall::new(
107            "call_123",
108            "calculator",
109            json!({"expression": "2 + 3"}).to_string(),
110        );
111
112        assert_eq!(call.id, "call_123");
113        assert_eq!(call.name(), "calculator");
114
115        let args: HashMap<String, String> = call.parse_arguments().unwrap();
116        assert_eq!(args.get("expression").unwrap(), "2 + 3");
117    }
118
119    #[test]
120    fn test_parse_arguments_tolerates_messy_llm_json() {
121        // LLM-generated arguments with trailing comma + trailing garbage
122        let call = ToolCall::new(
123            "call_456",
124            "weather",
125            r#"{"city": "beijing", "unit": "celsius",} plus extra text"#,
126        );
127
128        let args: HashMap<String, String> = call.parse_arguments().unwrap();
129        assert_eq!(args.get("city").unwrap(), "beijing");
130        assert_eq!(args.get("unit").unwrap(), "celsius");
131    }
132
133    #[test]
134    fn test_tool_call_result() {
135        let result = ToolCallResult::new("call_123", "5");
136
137        assert_eq!(result.tool_call_id, "call_123");
138        assert_eq!(result.role, "tool");
139        assert_eq!(result.content, "5");
140    }
141}