use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value};
use super::{FunctionCallResponse, FunctionDetail};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String,
}
impl ToolCall {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
arguments: arguments.into(),
}
}
pub fn from_value(id: impl Into<String>, name: impl Into<String>, arguments: &Value) -> Self {
Self::new(id, name, arguments.to_string())
}
pub fn arguments_value(&self) -> Value {
if self.arguments.trim().is_empty() {
return Value::Object(Default::default());
}
match serde_json::from_str::<Value>(&self.arguments) {
Ok(v @ Value::Object(_)) => v,
Ok(other) => json!({ "input": other }),
Err(_) => json!({ "input": self.arguments }),
}
}
pub fn parse_list(s: &str) -> Option<Vec<ToolCall>> {
let trimmed = s.trim_start();
if !trimmed.starts_with('[') {
return None;
}
serde_json::from_str(trimmed).ok()
}
pub fn list_to_string(calls: &[ToolCall]) -> String {
serde_json::to_string(calls).unwrap_or_else(|_| "[]".into())
}
}
impl From<FunctionCallResponse> for ToolCall {
fn from(r: FunctionCallResponse) -> Self {
Self {
id: r.id,
name: r.function.name,
arguments: r.function.arguments,
}
}
}
impl From<ToolCall> for FunctionCallResponse {
fn from(c: ToolCall) -> Self {
FunctionCallResponse {
id: c.id,
type_field: "function".into(),
function: FunctionDetail {
name: c.name,
arguments: c.arguments,
},
}
}
}
impl Serialize for ToolCall {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
FunctionCallResponse::from(self.clone()).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ToolCall {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
FunctionCallResponse::deserialize(deserializer).map(Into::into)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_to_the_openai_wire_format() {
let call = ToolCall::new("call_1", "weather", r#"{"city":"Lima"}"#);
let v = serde_json::to_value(&call).unwrap();
assert_eq!(
v,
json!({"id": "call_1", "type": "function",
"function": {"name": "weather", "arguments": "{\"city\":\"Lima\"}"}})
);
let back: ToolCall = serde_json::from_value(v).unwrap();
assert_eq!(back, call);
}
#[test]
fn parse_list_accepts_arrays_only() {
let calls = ToolCall::parse_list(
r#" [{"id":"a","type":"function","function":{"name":"f","arguments":"{}"}}]"#,
)
.unwrap();
assert_eq!(calls, vec![ToolCall::new("a", "f", "{}")]);
assert_eq!(ToolCall::parse_list("[]"), Some(vec![]));
assert_eq!(ToolCall::parse_list("Hello"), None);
assert_eq!(ToolCall::parse_list("[1, 2]"), None);
assert_eq!(ToolCall::parse_list(r#"{"id":"a"}"#), None);
}
#[test]
fn list_round_trip() {
let calls = vec![
ToolCall::from_value("a", "f", &json!({"x": 1})),
ToolCall::new("b", "g", ""),
];
let s = ToolCall::list_to_string(&calls);
assert_eq!(ToolCall::parse_list(&s).unwrap(), calls);
}
#[test]
fn arguments_value_normalizes_input() {
assert_eq!(ToolCall::new("", "f", "").arguments_value(), json!({}));
assert_eq!(
ToolCall::new("", "f", r#"{"a":1}"#).arguments_value(),
json!({"a": 1})
);
assert_eq!(
ToolCall::new("", "f", "[1]").arguments_value(),
json!({"input": [1]})
);
assert_eq!(
ToolCall::new("", "f", "plain text").arguments_value(),
json!({"input": "plain text"})
);
}
}