Skip to main content

tiny_agent_rs/core/
tool_call.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::time::{Duration, Instant};
4
5/// Represents a tool call request from the LLM
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ToolCall {
8    /// Unique identifier for this tool call
9    pub id: String,
10    /// Name of the tool to execute
11    pub name: String,
12    /// Arguments to pass to the tool
13    pub arguments: Value,
14}
15
16impl ToolCall {
17    /// Create a new tool call
18    pub fn new(id: String, name: String, arguments: Value) -> Self {
19        Self {
20            id,
21            name,
22            arguments,
23        }
24    }
25
26    /// Parse a tool call from OpenAI response format
27    pub fn from_openai_format(tool_call: &Value) -> Option<Self> {
28        let id = tool_call.get("id")?.as_str()?.to_string();
29        let function = tool_call.get("function")?;
30        let name = function.get("name")?.as_str()?.to_string();
31
32        let arguments_str = function.get("arguments")?.as_str()?;
33        let arguments: Value = serde_json::from_str(arguments_str).ok()?;
34
35        Some(Self {
36            id,
37            name,
38            arguments,
39        })
40    }
41
42    /// Convert to OpenAI tool call format
43    pub fn to_openai_format(&self) -> Value {
44        serde_json::json!({
45            "id": self.id,
46            "type": "function",
47            "function": {
48                "name": self.name,
49                "arguments": serde_json::to_string(&self.arguments).unwrap_or_default()
50            }
51        })
52    }
53
54    /// Get a human-readable description
55    pub fn describe(&self) -> String {
56        format!("{}({})", self.name, self.arguments)
57    }
58}
59
60/// Represents the output from a tool execution
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ToolOutput {
63    /// The tool call ID this output corresponds to
64    pub tool_call_id: String,
65    /// The tool name that was executed
66    pub tool_name: String,
67    /// The output/result from the tool
68    pub output: Value,
69    /// Whether this is the final answer
70    pub is_final: bool,
71    /// Whether the execution resulted in an error
72    pub is_error: bool,
73    /// Execution duration in milliseconds
74    pub duration_ms: Option<u128>,
75}
76
77impl ToolOutput {
78    /// Create a successful tool output
79    pub fn success(tool_call_id: String, tool_name: String, output: Value) -> Self {
80        Self {
81            tool_call_id,
82            tool_name,
83            output,
84            is_final: false,
85            is_error: false,
86            duration_ms: None,
87        }
88    }
89
90    /// Create an error tool output
91    pub fn error(tool_call_id: String, tool_name: String, error_msg: String) -> Self {
92        Self {
93            tool_call_id,
94            tool_name,
95            output: serde_json::json!({
96                "error": {
97                    "message": error_msg
98                }
99            }),
100            is_final: false,
101            is_error: true,
102            duration_ms: None,
103        }
104    }
105
106    /// Set the execution duration
107    pub fn with_duration(mut self, duration: Duration) -> Self {
108        self.duration_ms = Some(duration.as_millis());
109        self
110    }
111
112    /// Mark this as a final answer
113    pub fn as_final(mut self) -> Self {
114        self.is_final = true;
115        self
116    }
117
118    /// Get the output as a string for message content
119    pub fn as_string(&self) -> String {
120        match &self.output {
121            Value::String(s) => s.clone(),
122            other => other.to_string(),
123        }
124    }
125
126    /// Convert to OpenAI tool message format
127    pub fn to_openai_message(&self) -> Value {
128        serde_json::json!({
129            "role": "tool",
130            "tool_call_id": self.tool_call_id,
131            "content": self.as_string()
132        })
133    }
134}
135
136/// Tracks the execution of a tool call with timing information
137#[derive(Debug)]
138pub struct ToolExecution {
139    pub tool_call: ToolCall,
140    start_time: Instant,
141}
142
143impl ToolExecution {
144    /// Start tracking a tool execution
145    pub fn start(tool_call: ToolCall) -> Self {
146        Self {
147            tool_call,
148            start_time: Instant::now(),
149        }
150    }
151
152    /// Complete the execution and get the output with timing
153    pub fn complete(self, output: Value, is_error: bool) -> ToolOutput {
154        let duration = self.start_time.elapsed();
155        ToolOutput {
156            tool_call_id: self.tool_call.id,
157            tool_name: self.tool_call.name,
158            output,
159            is_final: false,
160            is_error,
161            duration_ms: Some(duration.as_millis()),
162        }
163    }
164
165    /// Complete with error
166    pub fn complete_with_error(self, error_msg: String) -> ToolOutput {
167        let duration = self.start_time.elapsed();
168        ToolOutput::error(self.tool_call.id, self.tool_call.name, error_msg).with_duration(duration)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_tool_call_creation() {
178        let call = ToolCall::new(
179            "call_123".to_string(),
180            "test_tool".to_string(),
181            serde_json::json!({"arg": "value"}),
182        );
183        assert_eq!(call.id, "call_123");
184        assert_eq!(call.name, "test_tool");
185    }
186
187    #[test]
188    fn test_tool_call_from_openai() {
189        let openai_format = serde_json::json!({
190            "id": "call_456",
191            "type": "function",
192            "function": {
193                "name": "calculator",
194                "arguments": "{\"expression\": \"2+2\"}"
195            }
196        });
197
198        let call = ToolCall::from_openai_format(&openai_format).unwrap();
199        assert_eq!(call.id, "call_456");
200        assert_eq!(call.name, "calculator");
201        assert_eq!(call.arguments["expression"], "2+2");
202    }
203
204    #[test]
205    fn test_tool_output_success() {
206        let output = ToolOutput::success(
207            "call_789".to_string(),
208            "test".to_string(),
209            serde_json::json!("result"),
210        );
211        assert!(!output.is_error);
212        assert!(!output.is_final);
213        assert_eq!(output.tool_call_id, "call_789");
214    }
215
216    #[test]
217    fn test_tool_output_error() {
218        let output = ToolOutput::error(
219            "call_999".to_string(),
220            "test".to_string(),
221            "Something went wrong".to_string(),
222        );
223        assert!(output.is_error);
224        assert_eq!(output.tool_name, "test");
225    }
226
227    #[test]
228    fn test_tool_execution_timing() {
229        let call = ToolCall::new("call_123".to_string(), "test".to_string(), Value::Null);
230        let execution = ToolExecution::start(call);
231        let output = execution.complete(serde_json::json!("result"), false);
232        assert!(output.duration_ms.is_some());
233    }
234}