use serde_json::{Map, Value};
use super::super::ToolDefinition;
pub(crate) const ARG_KEY: &str = "<arg_key>";
const ARG_KEY_END: &str = "</arg_key>";
pub(super) const ARG_VALUE: &str = "<arg_value>";
pub(super) const ARG_VALUE_END: &str = "</arg_value>";
pub(super) fn arg_pairs(raw: &str) -> Vec<(&str, &str)> {
let mut pairs = Vec::new();
let mut from = 0;
while let Some(rel) = raw[from..].find(ARG_KEY) {
let open = from + rel;
match pair_at(raw, open + ARG_KEY.len()) {
Some((key, value, end)) => {
pairs.push((key, value));
from = end;
}
None => from = open + 1,
}
}
pairs
}
fn pair_at(raw: &str, key_start: usize) -> Option<(&str, &str, usize)> {
let mut search = key_start;
while let Some(rel) = raw[search..].find(ARG_KEY_END) {
let key_end = search + rel;
let after_key = &raw[key_end + ARG_KEY_END.len()..];
if let Some(value_tail) = after_key.trim_start().strip_prefix(ARG_VALUE) {
let value_start = raw.len() - value_tail.len();
let value_len = value_tail.find(ARG_VALUE_END)?;
let value_end = value_start + value_len;
return Some((
&raw[key_start..key_end],
&raw[value_start..value_end],
value_end + ARG_VALUE_END.len(),
));
}
search = key_end + 1;
}
None
}
pub(super) fn convert_args(raw: &str) -> Map<String, Value> {
let mut args = Map::new();
for (key, value) in arg_pairs(raw) {
args.insert(key.trim().to_string(), Value::String(value.to_string()));
}
args
}
pub(super) fn tool_properties<'t>(
tools: &'t [ToolDefinition],
name: &str,
) -> Option<&'t Map<String, Value>> {
tools
.iter()
.find(|t| t.function.name == name)?
.function
.parameters
.as_ref()?
.get("properties")?
.as_object()
}
pub(super) fn coerce_to_schema(
args: &mut Map<String, Value>,
tools: &[ToolDefinition],
name: &str,
) {
let Some(properties) = tool_properties(tools, name) else {
return;
};
for (key, value) in args.iter_mut() {
let Some(schema) = properties.get(key).filter(|s| s.is_object()) else {
continue;
};
if let Value::String(s) = value
&& let Some(converted) = coerce_string(s, &schema_types(schema))
{
*value = converted;
}
}
}
fn schema_types(schema: &Value) -> Vec<String> {
let mut types = Vec::new();
collect_types(schema, &mut types);
if types.is_empty() {
types.push("string".to_string());
}
types
}
fn collect_types(schema: &Value, types: &mut Vec<String>) {
let Some(obj) = schema.as_object() else {
return;
};
let mut add = |t: &str| {
if !types.iter().any(|known| known == t) {
types.push(t.to_string());
}
};
match obj.get("type") {
Some(Value::String(t)) => add(t),
Some(Value::Array(list)) => list.iter().filter_map(Value::as_str).for_each(&mut add),
_ => {}
}
if let Some(Value::Array(values)) = obj.get("enum") {
for v in values {
add(match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(n) if n.is_f64() => "number",
Value::Number(_) => "integer",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
});
}
}
for branch in ["anyOf", "oneOf", "allOf"] {
if let Some(Value::Array(choices)) = obj.get(branch) {
for choice in choices {
collect_types(choice, types);
}
}
}
}
fn canonical_type(t: &str) -> String {
let t = t.trim().to_lowercase();
let canonical = match t.as_str() {
"str" | "text" | "varchar" | "char" | "enum" => "string",
"int" | "int32" | "int64" | "uint" | "uint32" | "uint64" | "long" | "short"
| "unsigned" => "integer",
"float" | "float32" | "float64" | "double" => "number",
"bool" => "boolean",
"dict" => "object",
"arr" | "list" | "sequence" => "array",
_ => return t,
};
canonical.to_string()
}
fn coerce_string(raw: &str, types: &[String]) -> Option<Value> {
let types: Vec<String> = types.iter().map(|t| canonical_type(t)).collect();
let has = |t: &str| types.iter().any(|known| known == t);
if has("null") && raw.to_lowercase() == "null" {
return Some(Value::Null);
}
if has("integer")
&& let Some(n) = parse_integer(raw)
{
return Some(n);
}
if has("number")
&& let Ok(f) = raw.trim().parse::<f64>()
&& f.is_finite()
{
return Some(number_value(f));
}
if has("boolean") {
match raw.trim().to_lowercase().as_str() {
"true" | "1" => return Some(Value::Bool(true)),
"false" | "0" => return Some(Value::Bool(false)),
_ => {}
}
}
if (has("object") || has("array"))
&& let Ok(parsed) = serde_json::from_str::<Value>(raw)
{
return Some(parsed);
}
if has("string") {
return None;
}
serde_json::from_str::<Value>(raw).ok()
}
fn parse_integer(raw: &str) -> Option<Value> {
let t = raw.trim();
if let Ok(i) = t.parse::<i64>() {
return Some(Value::from(i));
}
t.parse::<u64>().ok().map(Value::from)
}
fn number_value(f: f64) -> Value {
const I64_BOUND: f64 = 9_223_372_036_854_775_808.0;
if f.fract() == 0.0 && f.abs() < I64_BOUND {
Value::from(f as i64)
} else {
serde_json::Number::from_f64(f).map_or(Value::Null, Value::Number)
}
}