#[derive(Debug, Clone, PartialEq)]
pub struct ParsedToolCall {
pub name: String,
pub arguments: String,
}
pub fn tools_system_block(tools: &[serde_json::Value]) -> String {
let mut s = String::from(
"# Tools\n\nYou may call one or more functions to assist with the user query.\n\n\
You are provided with function signatures within <tools></tools> XML tags:\n<tools>",
);
for t in tools {
s.push('\n');
s.push_str(&serde_json::to_string(t).unwrap_or_default());
}
s.push_str(
"\n</tools>\n\nFor each function call, return a json object with function name and \
arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n\
{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>",
);
s
}
pub fn parse_tool_calls(text: &str) -> (String, Vec<ParsedToolCall>) {
const OPEN: &str = "<tool_call>";
const CLOSE: &str = "</tool_call>";
let mut calls = Vec::new();
let mut content = String::new();
let mut rest = text;
while let Some(start) = rest.find(OPEN) {
content.push_str(&rest[..start]);
let after = &rest[start + OPEN.len()..];
match after.find(CLOSE) {
Some(end) => {
if let Some(c) = parse_one(after[..end].trim()) {
calls.push(c);
}
rest = &after[end + CLOSE.len()..];
}
None => {
if let Some(c) = parse_one(after.trim()) {
calls.push(c);
}
rest = "";
break;
}
}
}
content.push_str(rest);
(content.trim().to_string(), calls)
}
fn parse_one(body: &str) -> Option<ParsedToolCall> {
let v: serde_json::Value = serde_json::from_str(body).ok()?;
let name = v.get("name")?.as_str()?.to_string();
let arguments = match v.get("arguments") {
Some(serde_json::Value::String(s)) => s.clone(),
Some(other) => serde_json::to_string(other).ok()?,
None => "{}".to_string(),
};
Some(ParsedToolCall { name, arguments })
}
#[cfg(test)]
mod tests {
use super::*;
fn call(name: &str, args: &str) -> ParsedToolCall {
ParsedToolCall {
name: name.to_string(),
arguments: args.to_string(),
}
}
#[test]
fn system_block_lists_each_signature_and_the_call_convention() {
let tools = vec![serde_json::json!({
"type": "function",
"function": {"name": "get_weather", "parameters": {"type": "object"}}
})];
let s = tools_system_block(&tools);
assert!(
s.contains("<tools>\n{"),
"signatures live inside <tools>: {s}"
);
assert!(
s.contains("\"name\":\"get_weather\""),
"compact json line: {s}"
);
assert!(s.contains("</tools>"));
assert!(
s.contains("<tool_call>"),
"call convention is spelled out: {s}"
);
}
#[test]
fn parses_a_single_tool_call_and_stringifies_object_arguments() {
let (content, calls) = parse_tool_calls(
"<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"Paris\"}}\n</tool_call>",
);
assert_eq!(content, "");
assert_eq!(calls, vec![call("get_weather", "{\"city\":\"Paris\"}")]);
}
#[test]
fn keeps_prose_outside_the_blocks_as_content() {
let (content, calls) = parse_tool_calls(
"Let me check.\n<tool_call>\n{\"name\": \"f\", \"arguments\": {}}\n</tool_call>",
);
assert_eq!(content, "Let me check.");
assert_eq!(calls, vec![call("f", "{}")]);
}
#[test]
fn parses_multiple_calls() {
let (_, calls) = parse_tool_calls(
"<tool_call>\n{\"name\": \"a\", \"arguments\": {\"x\": 1}}\n</tool_call>\
<tool_call>\n{\"name\": \"b\", \"arguments\": {}}\n</tool_call>",
);
assert_eq!(calls, vec![call("a", "{\"x\":1}"), call("b", "{}")]);
}
#[test]
fn plain_reply_yields_no_calls() {
let (content, calls) = parse_tool_calls("The capital of France is Paris.");
assert_eq!(content, "The capital of France is Paris.");
assert!(calls.is_empty());
}
#[test]
fn missing_arguments_defaults_to_empty_object() {
let (_, calls) = parse_tool_calls("<tool_call>{\"name\": \"noop\"}</tool_call>");
assert_eq!(calls, vec![call("noop", "{}")]);
}
#[test]
fn malformed_block_is_dropped_not_leaked_as_content() {
let (content, calls) = parse_tool_calls("<tool_call>not json</tool_call>");
assert_eq!(content, "");
assert!(calls.is_empty());
}
#[test]
fn unterminated_trailing_block_is_parsed_best_effort() {
let (_, calls) =
parse_tool_calls("<tool_call>\n{\"name\": \"f\", \"arguments\": {\"a\": 1}}");
assert_eq!(calls, vec![call("f", "{\"a\":1}")]);
}
}