use anyhow::{bail, Result};
use serde_json::Value;
use crate::api::types::{ToolCall, ToolDefinition};
use crate::errors::ToolError;
pub fn validate_tool_call(call: &ToolCall, definitions: &[ToolDefinition]) -> Result<()> {
call.validate_structure()?;
let tool_name = call.function.name.trim();
let definition = definitions
.iter()
.find(|d| d.function.name == tool_name)
.ok_or_else(|| ToolError::InvalidToolCall {
name: tool_name.to_string(),
message: format!(
"Unknown tool '{}'. Available tools: {:?}",
tool_name,
definitions
.iter()
.map(|d| &d.function.name)
.collect::<Vec<_>>()
),
})?;
let args: Value =
serde_json::from_str(&call.function.arguments).map_err(|e| ToolError::InvalidToolCall {
name: tool_name.to_string(),
message: format!("Arguments are not valid JSON: {}", e),
})?;
let args_obj = args
.as_object()
.ok_or_else(|| ToolError::InvalidArguments {
name: tool_name.to_string(),
message: "Tool arguments must be a JSON object".to_string(),
})?;
let schema = &definition.function.parameters;
if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
for req in required {
if let Some(req_str) = req.as_str() {
if !args_obj.contains_key(req_str) {
return Err(ToolError::InvalidArguments {
name: tool_name.to_string(),
message: format!("Missing required argument '{}'", req_str),
}
.into());
}
}
}
}
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
for (prop_name, prop_schema) in properties {
if let Some(arg_val) = args_obj.get(prop_name) {
if let Some(prop_type) = prop_schema.get("type") {
if !value_matches_schema_type(arg_val, prop_type) {
return Err(ToolError::InvalidArguments {
name: tool_name.to_string(),
message: format!(
"Argument '{}' expected type '{}' but got incompatible value",
prop_name, prop_type
),
}
.into());
}
}
if let Some(enum_values) = prop_schema.get("enum").and_then(|e| e.as_array()) {
if !enum_values.iter().any(|allowed| allowed == arg_val) {
return Err(ToolError::InvalidArguments {
name: tool_name.to_string(),
message: format!(
"Argument '{}' must be one of {:?}",
prop_name, enum_values
),
}
.into());
}
}
}
}
}
Ok(())
}
fn value_matches_schema_type(value: &Value, schema_type: &Value) -> bool {
if let Some(type_str) = schema_type.as_str() {
return value_matches_single_type(value, type_str);
}
if let Some(types) = schema_type.as_array() {
return types.iter().any(|t| value_matches_schema_type(value, t));
}
true
}
fn value_matches_single_type(value: &Value, schema_type: &str) -> bool {
match schema_type {
"string" => value.is_string(),
"integer" => value.is_i64() || value.is_u64(),
"number" => value.is_number(),
"boolean" => value.is_boolean(),
"array" => value.is_array(),
"object" => value.is_object(),
"null" => value.is_null(),
_ => true, }
}
pub fn validate_tool_calls(calls: &[ToolCall], definitions: &[ToolDefinition]) -> Result<()> {
let mut errors: Vec<String> = Vec::new();
for (i, call) in calls.iter().enumerate() {
if let Err(e) = validate_tool_call(call, definitions) {
errors.push(format!("[call {}] {}", i + 1, e));
}
}
if errors.is_empty() {
Ok(())
} else {
bail!("Tool call validation failed:\n{}", errors.join("\n"));
}
}
#[cfg(test)]
#[path = "../../tests/unit/agent/tool_validator/tool_validator_test.rs"]
mod tests;