Skip to main content

langchain_rust/schemas/
tool_call.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::{json, Value};
3
4use super::{FunctionCallResponse, FunctionDetail};
5
6/// A tool/function call requested by a model, the same shape for every provider
7/// (OpenAI, Claude, Ollama, Bedrock, ...).
8///
9/// `arguments` is the raw JSON string produced by the model; only the tool knows how to
10/// interpret it. Serializes to the OpenAI wire format
11/// (`{"id","type":"function","function":{"name","arguments"}}`), which is also what
12/// [`Message::tool_calls`](super::Message) holds.
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct ToolCall {
15    pub id: String,
16    pub name: String,
17    pub arguments: String,
18}
19
20impl ToolCall {
21    pub fn new(
22        id: impl Into<String>,
23        name: impl Into<String>,
24        arguments: impl Into<String>,
25    ) -> Self {
26        Self {
27            id: id.into(),
28            name: name.into(),
29            arguments: arguments.into(),
30        }
31    }
32
33    /// Builds a call from structured arguments (what Claude, Ollama and Bedrock return).
34    pub fn from_value(id: impl Into<String>, name: impl Into<String>, arguments: &Value) -> Self {
35        Self::new(id, name, arguments.to_string())
36    }
37
38    /// The arguments as a JSON object, for providers that take tool input as an object
39    /// (Anthropic, Ollama, Bedrock). Non-object JSON or plain text is wrapped as
40    /// `{"input": ...}`, and empty arguments become `{}`.
41    pub fn arguments_value(&self) -> Value {
42        if self.arguments.trim().is_empty() {
43            return Value::Object(Default::default());
44        }
45        match serde_json::from_str::<Value>(&self.arguments) {
46            Ok(v @ Value::Object(_)) => v,
47            Ok(other) => json!({ "input": other }),
48            Err(_) => json!({ "input": self.arguments }),
49        }
50    }
51
52    /// Parses an OpenAI-style JSON array of tool calls (e.g. a `generation` string from an
53    /// LLM that encodes tool calls as text). Returns `None` if it is not one.
54    pub fn parse_list(s: &str) -> Option<Vec<ToolCall>> {
55        let trimmed = s.trim_start();
56        if !trimmed.starts_with('[') {
57            return None;
58        }
59        serde_json::from_str(trimmed).ok()
60    }
61
62    /// Serializes a list of calls to the OpenAI-style JSON array.
63    pub fn list_to_string(calls: &[ToolCall]) -> String {
64        serde_json::to_string(calls).unwrap_or_else(|_| "[]".into())
65    }
66}
67
68impl From<FunctionCallResponse> for ToolCall {
69    fn from(r: FunctionCallResponse) -> Self {
70        Self {
71            id: r.id,
72            name: r.function.name,
73            arguments: r.function.arguments,
74        }
75    }
76}
77
78impl From<ToolCall> for FunctionCallResponse {
79    fn from(c: ToolCall) -> Self {
80        FunctionCallResponse {
81            id: c.id,
82            type_field: "function".into(),
83            function: FunctionDetail {
84                name: c.name,
85                arguments: c.arguments,
86            },
87        }
88    }
89}
90
91impl Serialize for ToolCall {
92    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
93        FunctionCallResponse::from(self.clone()).serialize(serializer)
94    }
95}
96
97impl<'de> Deserialize<'de> for ToolCall {
98    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99        FunctionCallResponse::deserialize(deserializer).map(Into::into)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn serializes_to_the_openai_wire_format() {
109        let call = ToolCall::new("call_1", "weather", r#"{"city":"Lima"}"#);
110        let v = serde_json::to_value(&call).unwrap();
111        assert_eq!(
112            v,
113            json!({"id": "call_1", "type": "function",
114                   "function": {"name": "weather", "arguments": "{\"city\":\"Lima\"}"}})
115        );
116        let back: ToolCall = serde_json::from_value(v).unwrap();
117        assert_eq!(back, call);
118    }
119
120    #[test]
121    fn parse_list_accepts_arrays_only() {
122        let calls = ToolCall::parse_list(
123            r#" [{"id":"a","type":"function","function":{"name":"f","arguments":"{}"}}]"#,
124        )
125        .unwrap();
126        assert_eq!(calls, vec![ToolCall::new("a", "f", "{}")]);
127        assert_eq!(ToolCall::parse_list("[]"), Some(vec![]));
128        assert_eq!(ToolCall::parse_list("Hello"), None);
129        assert_eq!(ToolCall::parse_list("[1, 2]"), None);
130        assert_eq!(ToolCall::parse_list(r#"{"id":"a"}"#), None);
131    }
132
133    #[test]
134    fn list_round_trip() {
135        let calls = vec![
136            ToolCall::from_value("a", "f", &json!({"x": 1})),
137            ToolCall::new("b", "g", ""),
138        ];
139        let s = ToolCall::list_to_string(&calls);
140        assert_eq!(ToolCall::parse_list(&s).unwrap(), calls);
141    }
142
143    #[test]
144    fn arguments_value_normalizes_input() {
145        assert_eq!(ToolCall::new("", "f", "").arguments_value(), json!({}));
146        assert_eq!(
147            ToolCall::new("", "f", r#"{"a":1}"#).arguments_value(),
148            json!({"a": 1})
149        );
150        assert_eq!(
151            ToolCall::new("", "f", "[1]").arguments_value(),
152            json!({"input": [1]})
153        );
154        assert_eq!(
155            ToolCall::new("", "f", "plain text").arguments_value(),
156            json!({"input": "plain text"})
157        );
158    }
159}