use std::path::Path;
use serde_json::{Map, Value};
use crate::protocol_policy::tool_definition_name;
pub fn response_arguments_for_tool(
tools: &[Value],
tool_name: &str,
arguments: String,
user_prompt: &str,
) -> String {
let Some(schema) = tools.iter().find_map(|tool| {
(tool_definition_name(tool).as_deref() == Some(tool_name))
.then(|| tool_parameters_schema(tool))
.flatten()
}) else {
return arguments;
};
let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
return arguments;
};
if properties.is_empty() {
return arguments;
}
let Ok(source) = serde_json::from_str::<Value>(&arguments) else {
return arguments;
};
let Some(source) = source.as_object() else {
return arguments;
};
let required = schema
.get("required")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let mut projected = Map::new();
for (name, property_schema) in properties {
let value = source
.get(name)
.cloned()
.or_else(|| semantic_alias(name, source, user_prompt))
.or_else(|| {
required
.iter()
.any(|entry| entry.as_str() == Some(name))
.then(|| schema_default(property_schema, name, user_prompt))
});
if let Some(value) = value {
projected.insert(name.clone(), constrain_to_schema(value, property_schema));
}
}
let projected = Value::Object(projected).to_string();
if std::env::var("FORMAL_AI_TRACE_REQUESTS").as_deref() == Ok("1") && projected != arguments {
eprintln!(
"[trace] tool_schema_projection: tool={tool_name} planned={arguments} emitted={projected}"
);
}
projected
}
fn tool_parameters_schema(tool: &Value) -> Option<&Value> {
let object = tool.as_object()?;
object
.get("parameters")
.or_else(|| object.get("input_schema"))
.or_else(|| {
object
.get("function")
.and_then(|function| function.get("parameters"))
})
.or_else(|| {
object
.get("function")
.and_then(|function| function.get("input_schema"))
})
}
fn semantic_alias(name: &str, source: &Map<String, Value>, user_prompt: &str) -> Option<Value> {
let aliases: &[&str] = match name {
"path" | "filePath" | "file_path" | "absolute_path" => {
&["path", "filePath", "file_path", "absolute_path"]
}
"command" | "cmd" => &["command", "cmd"],
"query" | "pattern" => &["query", "pattern"],
"old" | "oldString" | "old_string" | "old_str" => {
&["old", "oldString", "old_string", "old_str"]
}
"new" | "newString" | "new_string" | "new_str" => {
&["new", "newString", "new_string", "new_str"]
}
"prompt" | "instruction" => return Some(Value::String(user_prompt.to_owned())),
_ => return None,
};
let mut value = aliases
.iter()
.find_map(|alias| source.get(*alias).cloned())?;
if name == "absolute_path" {
if let Some(path) = value.as_str() {
value = Value::String(absolute_path(path));
}
}
Some(value)
}
fn absolute_path(path: &str) -> String {
let path = Path::new(path);
if path.is_absolute() {
return path.to_string_lossy().into_owned();
}
std::path::absolute(path)
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.into_owned()
}
fn schema_default(schema: &Value, name: &str, user_prompt: &str) -> Value {
if let Some(default) = schema.get("default") {
return default.clone();
}
if let Some(first) = schema
.get("enum")
.and_then(Value::as_array)
.and_then(|e| e.first())
{
return first.clone();
}
match schema.get("type").and_then(Value::as_str) {
Some("boolean") => Value::Bool(name == "login"),
Some("array") => Value::Array(Vec::new()),
Some("object") => Value::Object(Map::new()),
Some("integer" | "number") => Value::from(0),
Some("null") => Value::Null,
_ if matches!(name, "prompt" | "instruction") => Value::String(user_prompt.to_owned()),
_ => Value::String(String::new()),
}
}
fn constrain_to_schema(value: Value, schema: &Value) -> Value {
let Some(allowed) = schema.get("enum").and_then(Value::as_array) else {
return value;
};
if allowed.contains(&value) {
value
} else {
allowed.first().cloned().unwrap_or(value)
}
}