mirror of
https://github.com/vllm-project/vllm.git
synced 2026-06-06 00:16:14 +00:00
[Rust Frontend] Support recursive tool parameter conversion (#44299)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
This commit is contained in:
@@ -104,7 +104,7 @@ impl DeepSeekDsmlToolParser {
|
||||
self.tool_parameters.convert_param_with_schema(
|
||||
&name,
|
||||
¶m.name,
|
||||
¶m.value,
|
||||
param.value,
|
||||
)
|
||||
};
|
||||
arguments.insert(param.name, value);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{Number, Value};
|
||||
use serde_json::{Map, Number, Value};
|
||||
|
||||
use crate::Tool;
|
||||
|
||||
@@ -21,6 +21,29 @@ pub(super) struct ToolSchema {
|
||||
params: BTreeMap<String, JsonParamType>,
|
||||
}
|
||||
|
||||
/// Parameter input for schema-aware conversion.
|
||||
///
|
||||
/// It can be either a raw text string, or a structured input with named child elements.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum ParamInput {
|
||||
Text(String),
|
||||
#[allow(dead_code)]
|
||||
Elements(Vec<ParamElement>),
|
||||
}
|
||||
|
||||
impl From<String> for ParamInput {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Text(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// One named structured parameter child.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct ParamElement {
|
||||
pub name: String,
|
||||
pub value: ParamInput,
|
||||
}
|
||||
|
||||
/// Normalized JSON parameter type used for raw string coercion.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum JsonParamType {
|
||||
@@ -28,8 +51,13 @@ pub(super) enum JsonParamType {
|
||||
Integer,
|
||||
Number,
|
||||
Boolean,
|
||||
Object,
|
||||
Array,
|
||||
Object {
|
||||
properties: BTreeMap<String, JsonParamType>,
|
||||
additional_properties: Option<Box<JsonParamType>>,
|
||||
},
|
||||
Array {
|
||||
items: Option<Box<JsonParamType>>,
|
||||
},
|
||||
Null,
|
||||
OneOf(Vec<JsonParamType>),
|
||||
}
|
||||
@@ -45,33 +73,39 @@ impl ToolSchemas {
|
||||
Self { tools }
|
||||
}
|
||||
|
||||
/// Convert raw string parameter values for one named tool.
|
||||
/// Convert parameter values for one named tool.
|
||||
///
|
||||
/// Unknown tool names use an empty schema, so all parameters fall back to
|
||||
/// strings.
|
||||
pub(super) fn convert_params_with_schema(
|
||||
/// strings or object-like JSON for structured inputs.
|
||||
pub(super) fn convert_params_with_schema<P>(
|
||||
&self,
|
||||
function_name: &str,
|
||||
params: Vec<(String, String)>,
|
||||
) -> serde_json::Map<String, Value> {
|
||||
params: Vec<(String, P)>,
|
||||
) -> Map<String, Value>
|
||||
where
|
||||
P: Into<ParamInput>,
|
||||
{
|
||||
let tool_schema = self.tools.get(function_name).unwrap_or(ToolSchema::empty());
|
||||
let mut converted = serde_json::Map::with_capacity(params.len());
|
||||
let mut converted = Map::with_capacity(params.len());
|
||||
for (name, value) in params {
|
||||
let value = tool_schema.convert(&name, &value);
|
||||
let value = tool_schema.convert(&name, value.into());
|
||||
converted.insert(name, value);
|
||||
}
|
||||
converted
|
||||
}
|
||||
|
||||
/// Convert one raw string parameter value for one named tool.
|
||||
pub(super) fn convert_param_with_schema(
|
||||
/// Convert one parameter value for one named tool.
|
||||
pub(super) fn convert_param_with_schema<P>(
|
||||
&self,
|
||||
function_name: &str,
|
||||
name: &str,
|
||||
value: &str,
|
||||
) -> Value {
|
||||
value: P,
|
||||
) -> Value
|
||||
where
|
||||
P: Into<ParamInput>,
|
||||
{
|
||||
let tool_schema = self.tools.get(function_name).unwrap_or(ToolSchema::empty());
|
||||
tool_schema.convert(name, value)
|
||||
tool_schema.convert(name, value.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,21 +135,13 @@ impl ToolSchema {
|
||||
Self { params }
|
||||
}
|
||||
|
||||
/// Convert one raw parameter value using its normalized schema type.
|
||||
/// Convert one parameter value using its normalized schema type.
|
||||
///
|
||||
/// If the parameter name is unknown, or we don't have a schema for it, or
|
||||
/// the value fails to convert, this falls back to returning the raw
|
||||
/// string as a JSON string value.
|
||||
fn convert(&self, name: &str, value: &str) -> Value {
|
||||
if value.eq_ignore_ascii_case("null") {
|
||||
return Value::Null;
|
||||
}
|
||||
|
||||
let Some(param_type) = self.params.get(name) else {
|
||||
return Value::String(value.to_string());
|
||||
};
|
||||
|
||||
convert_value(param_type, value).unwrap_or_else(|| Value::String(value.to_string()))
|
||||
/// string as a JSON string value, or object-like JSON for structured input.
|
||||
fn convert(&self, name: &str, input: ParamInput) -> Value {
|
||||
convert_with_optional_schema(self.params.get(name), &input)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +151,7 @@ impl JsonParamType {
|
||||
let schema = schema.as_object()?;
|
||||
|
||||
if let Some(type_value) = schema.get("type") {
|
||||
return Self::from_type_value(type_value);
|
||||
return Self::from_type_value(type_value, schema);
|
||||
}
|
||||
|
||||
if let Some(composite) = schema.get("anyOf").or_else(|| schema.get("oneOf")) {
|
||||
@@ -134,32 +160,34 @@ impl JsonParamType {
|
||||
.map(|schemas| schemas.iter().filter_map(Self::from_schema).collect::<Vec<_>>())
|
||||
.filter(|types| !types.is_empty())
|
||||
.map(Self::one_of)
|
||||
.unwrap_or(Self::Object);
|
||||
.unwrap_or_else(|| Self::object_from_schema(Some(schema)));
|
||||
return Some(param_type);
|
||||
}
|
||||
|
||||
// Typically, these types are already handled by checking the "type" field, but
|
||||
// we can also infer them from their characteristic fields if "type" is missing.
|
||||
if schema.contains_key("enum") {
|
||||
return Some(Self::String);
|
||||
}
|
||||
if schema.contains_key("items") {
|
||||
return Some(Self::Array);
|
||||
return Some(Self::array_from_schema(Some(schema)));
|
||||
}
|
||||
if schema.contains_key("properties") {
|
||||
return Some(Self::Object);
|
||||
if schema.contains_key("properties") || schema.contains_key("additionalProperties") {
|
||||
return Some(Self::object_from_schema(Some(schema)));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Normalize a JSON schema `type` value.
|
||||
fn from_type_value(type_value: &Value) -> Option<Self> {
|
||||
fn from_type_value(type_value: &Value, schema: &Map<String, Value>) -> Option<Self> {
|
||||
match type_value {
|
||||
Value::String(kind) => Self::from_type_name(kind),
|
||||
Value::String(kind) => Self::from_type_name(kind, Some(schema)),
|
||||
Value::Array(kinds) => {
|
||||
let types = kinds
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.filter_map(Self::from_type_name)
|
||||
.filter_map(|kind| Self::from_type_name(kind, Some(schema)))
|
||||
.collect::<Vec<_>>();
|
||||
if types.is_empty() {
|
||||
None
|
||||
@@ -172,15 +200,15 @@ impl JsonParamType {
|
||||
}
|
||||
|
||||
/// Normalize one JSON schema type name.
|
||||
fn from_type_name(kind: &str) -> Option<Self> {
|
||||
fn from_type_name(kind: &str, schema: Option<&Map<String, Value>>) -> Option<Self> {
|
||||
let kind = kind.trim().to_ascii_lowercase();
|
||||
match kind.as_str() {
|
||||
"string" | "str" | "text" | "varchar" | "char" | "enum" => Some(Self::String),
|
||||
"integer" | "int" => Some(Self::Integer),
|
||||
"number" | "float" | "double" => Some(Self::Number),
|
||||
"boolean" | "bool" | "binary" => Some(Self::Boolean),
|
||||
"object" | "dict" | "map" => Some(Self::Object),
|
||||
"array" | "arr" | "list" | "sequence" => Some(Self::Array),
|
||||
"object" | "dict" | "map" => Some(Self::object_from_schema(schema)),
|
||||
"array" | "arr" | "list" | "sequence" => Some(Self::array_from_schema(schema)),
|
||||
"null" => Some(Self::Null),
|
||||
_ if kind.starts_with("int")
|
||||
|| kind.starts_with("uint")
|
||||
@@ -191,12 +219,52 @@ impl JsonParamType {
|
||||
Some(Self::Integer)
|
||||
}
|
||||
_ if kind.starts_with("num") || kind.starts_with("float") => Some(Self::Number),
|
||||
_ if kind.starts_with("dict") => Some(Self::Object),
|
||||
_ if kind.starts_with("list") => Some(Self::Array),
|
||||
_ if kind.starts_with("dict") => Some(Self::object_from_schema(schema)),
|
||||
_ if kind.starts_with("list") => Some(Self::array_from_schema(schema)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize object schema fields.
|
||||
fn object_from_schema(schema: Option<&Map<String, Value>>) -> Self {
|
||||
let properties = schema
|
||||
.and_then(|schema| schema.get("properties"))
|
||||
.and_then(Value::as_object)
|
||||
.map(|properties| {
|
||||
properties
|
||||
.iter()
|
||||
.filter_map(|(name, schema)| {
|
||||
Self::from_schema(schema).map(|param_type| (name.clone(), param_type))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let additional_properties =
|
||||
schema.and_then(|schema| schema.get("additionalProperties")).and_then(|schema| {
|
||||
if schema.is_object() {
|
||||
Self::from_schema(schema).map(Box::new)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Self::Object {
|
||||
properties,
|
||||
additional_properties,
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize array schema fields.
|
||||
fn array_from_schema(schema: Option<&Map<String, Value>>) -> Self {
|
||||
let items = schema
|
||||
.and_then(|schema| schema.get("items"))
|
||||
.and_then(Self::from_schema)
|
||||
.map(Box::new);
|
||||
|
||||
Self::Array { items }
|
||||
}
|
||||
|
||||
/// Collapse a candidate type list into one normalized type.
|
||||
fn one_of(mut types: Vec<Self>) -> Self {
|
||||
if types.len() == 1 {
|
||||
@@ -207,23 +275,126 @@ impl JsonParamType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert one raw string value to a normalized JSON type.
|
||||
fn convert_value(param_type: &JsonParamType, value: &str) -> Option<Value> {
|
||||
match param_type {
|
||||
JsonParamType::String => Some(Value::String(value.to_string())),
|
||||
JsonParamType::Integer => value.parse::<i64>().ok().map(Number::from).map(Value::Number),
|
||||
JsonParamType::Number => convert_number(value),
|
||||
JsonParamType::Boolean => convert_boolean(value),
|
||||
JsonParamType::Object | JsonParamType::Array => serde_json::from_str(value).ok(),
|
||||
JsonParamType::Null => value.eq_ignore_ascii_case("null").then_some(Value::Null),
|
||||
JsonParamType::OneOf(types) => {
|
||||
types.iter().find_map(|param_type| convert_value(param_type, value))
|
||||
/// Convert one parameter input to a normalized JSON value.
|
||||
fn convert_with_optional_schema(param_type: Option<&JsonParamType>, input: &ParamInput) -> Value {
|
||||
// For literal `null`, always convert to JSON null value.
|
||||
if let ParamInput::Text(value) = input
|
||||
&& value.eq_ignore_ascii_case("null")
|
||||
{
|
||||
return Value::Null;
|
||||
}
|
||||
|
||||
// If we have a schema, try to convert the value using it.
|
||||
if let Some(param_type) = param_type
|
||||
&& let Some(value) = try_convert_value(param_type, input)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
// We don't have a schema, or conversion failed, use fallback logic.
|
||||
match input {
|
||||
ParamInput::Text(value) => Value::String(value.clone()),
|
||||
ParamInput::Elements(elements) => {
|
||||
// Convert structured input to object without a schema.
|
||||
Value::Object(convert_elements_to_object(elements, &BTreeMap::new(), None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert one parameter input to a normalized JSON type.
|
||||
fn try_convert_value(param_type: &JsonParamType, input: &ParamInput) -> Option<Value> {
|
||||
match input {
|
||||
ParamInput::Text(value) => try_convert_text_value(param_type, value),
|
||||
ParamInput::Elements(elements) => try_convert_elements_value(param_type, elements),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert one raw string value to a normalized JSON type.
|
||||
fn try_convert_text_value(param_type: &JsonParamType, value: &str) -> Option<Value> {
|
||||
match param_type {
|
||||
JsonParamType::String => Some(Value::String(value.to_string())),
|
||||
JsonParamType::Integer => value.parse::<i64>().ok().map(Number::from).map(Value::Number),
|
||||
JsonParamType::Number => try_convert_number(value),
|
||||
JsonParamType::Boolean => try_convert_boolean(value),
|
||||
JsonParamType::Object { .. } if value.is_empty() => Some(Value::Object(Map::new())),
|
||||
JsonParamType::Array { .. } if value.is_empty() => Some(Value::Array(Vec::new())),
|
||||
JsonParamType::Object { .. } | JsonParamType::Array { .. } => {
|
||||
// For composite types with string input, simply interpret the string as JSON.
|
||||
serde_json::from_str(value).ok()
|
||||
}
|
||||
JsonParamType::Null => value.eq_ignore_ascii_case("null").then_some(Value::Null),
|
||||
JsonParamType::OneOf(types) => {
|
||||
types.iter().find_map(|param_type| try_convert_text_value(param_type, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert one structured parameter input to a normalized JSON type.
|
||||
fn try_convert_elements_value(
|
||||
param_type: &JsonParamType,
|
||||
elements: &[ParamElement],
|
||||
) -> Option<Value> {
|
||||
match param_type {
|
||||
JsonParamType::Object {
|
||||
properties,
|
||||
additional_properties,
|
||||
} => Some(Value::Object(convert_elements_to_object(
|
||||
elements,
|
||||
properties,
|
||||
additional_properties.as_deref(),
|
||||
))),
|
||||
JsonParamType::Array { items } => Some(Value::Array(
|
||||
// Collect all child elements into an array, regardless of their names.
|
||||
elements
|
||||
.iter()
|
||||
.map(|element| convert_with_optional_schema(items.as_deref(), &element.value))
|
||||
.collect(),
|
||||
)),
|
||||
JsonParamType::OneOf(types) => types
|
||||
.iter()
|
||||
.find_map(|param_type| try_convert_elements_value(param_type, elements)),
|
||||
|
||||
// Primitive types can't be converted from structured input.
|
||||
JsonParamType::String
|
||||
| JsonParamType::Integer
|
||||
| JsonParamType::Number
|
||||
| JsonParamType::Boolean
|
||||
| JsonParamType::Null => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert structured elements to an object, using field schemas when present.
|
||||
fn convert_elements_to_object(
|
||||
elements: &[ParamElement],
|
||||
properties: &BTreeMap<String, JsonParamType>,
|
||||
additional_properties: Option<&JsonParamType>,
|
||||
) -> Map<String, Value> {
|
||||
let mut object = Map::with_capacity(elements.len());
|
||||
for element in elements {
|
||||
let param_type = properties.get(&element.name).or(additional_properties);
|
||||
let value = convert_with_optional_schema(param_type, &element.value);
|
||||
insert_object_value(&mut object, element.name.clone(), value);
|
||||
}
|
||||
object
|
||||
}
|
||||
|
||||
/// Insert an object field while preserving duplicate keys as arrays.
|
||||
fn insert_object_value(object: &mut Map<String, Value>, key: String, value: Value) {
|
||||
if let Some(existing) = object.get_mut(&key) {
|
||||
match existing {
|
||||
// Collect values under the same key into an array.
|
||||
Value::Array(values) => values.push(value),
|
||||
existing => {
|
||||
let first = std::mem::replace(existing, Value::Null);
|
||||
*existing = Value::Array(vec![first, value]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
object.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert one raw string value to a JSON number.
|
||||
fn convert_number(value: &str) -> Option<Value> {
|
||||
fn try_convert_number(value: &str) -> Option<Value> {
|
||||
serde_json::from_str::<Number>(value)
|
||||
.or_else(|_| value.parse::<i64>().map(Number::from))
|
||||
.or_else(|_| value.parse::<f64>().ok().and_then(Number::from_f64).ok_or(()))
|
||||
@@ -232,7 +403,7 @@ fn convert_number(value: &str) -> Option<Value> {
|
||||
}
|
||||
|
||||
/// Convert one raw string value to a boolean.
|
||||
fn convert_boolean(value: &str) -> Option<Value> {
|
||||
fn try_convert_boolean(value: &str) -> Option<Value> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"true" | "1" => Some(Value::Bool(true)),
|
||||
"false" | "0" => Some(Value::Bool(false)),
|
||||
@@ -242,9 +413,9 @@ fn convert_boolean(value: &str) -> Option<Value> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{ToolSchema, ToolSchemas};
|
||||
use super::{ParamElement, ParamInput, ToolSchema, ToolSchemas};
|
||||
use crate::Tool;
|
||||
|
||||
fn test_tool(name: &str, parameters: serde_json::Value) -> Tool {
|
||||
@@ -260,8 +431,8 @@ mod tests {
|
||||
fn invalid_schema_converts_everything_as_string() {
|
||||
let params = ToolSchema::from_schema(&json!({ "type": "object" }));
|
||||
|
||||
assert_eq!(params.convert("count", "42"), json!("42"));
|
||||
assert_eq!(params.convert("count", "null"), json!(null));
|
||||
assert_eq!(params.convert("count", text("42")), json!("42"));
|
||||
assert_eq!(params.convert("count", text("null")), json!(null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,9 +446,9 @@ mod tests {
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(params.convert("unknown_schema", "42"), json!("42"));
|
||||
assert_eq!(params.convert("unknown_type", "42"), json!("42"));
|
||||
assert_eq!(params.convert("known", "42"), json!(42));
|
||||
assert_eq!(params.convert("unknown_schema", text("42")), json!("42"));
|
||||
assert_eq!(params.convert("unknown_type", text("42")), json!("42"));
|
||||
assert_eq!(params.convert("known", text("42")), json!(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -298,16 +469,25 @@ mod tests {
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(params.convert("text", "42"), json!("42"));
|
||||
assert_eq!(params.convert("count", "42"), json!(42));
|
||||
assert_eq!(params.convert("size", "5.0"), json!(5.0));
|
||||
assert_eq!(params.convert("ratio", "2.5"), json!(2.5));
|
||||
assert_eq!(params.convert("enabled", "1"), json!(true));
|
||||
assert_eq!(params.convert("payload", r#"{"k":1}"#), json!({ "k": 1 }));
|
||||
assert_eq!(params.convert("mapping", r#"{"k":1}"#), json!({ "k": 1 }));
|
||||
assert_eq!(params.convert("items", "[1,2]"), json!([1, 2]));
|
||||
assert_eq!(params.convert("names", r#"["a","b"]"#), json!(["a", "b"]));
|
||||
assert_eq!(params.convert("nothing", "null"), json!(null));
|
||||
assert_eq!(params.convert("text", text("42")), json!("42"));
|
||||
assert_eq!(params.convert("count", text("42")), json!(42));
|
||||
assert_eq!(params.convert("size", text("5.0")), json!(5.0));
|
||||
assert_eq!(params.convert("ratio", text("2.5")), json!(2.5));
|
||||
assert_eq!(params.convert("enabled", text("1")), json!(true));
|
||||
assert_eq!(
|
||||
params.convert("payload", text(r#"{"k":1}"#)),
|
||||
json!({ "k": 1 })
|
||||
);
|
||||
assert_eq!(
|
||||
params.convert("mapping", text(r#"{"k":1}"#)),
|
||||
json!({ "k": 1 })
|
||||
);
|
||||
assert_eq!(params.convert("items", text("[1,2]")), json!([1, 2]));
|
||||
assert_eq!(
|
||||
params.convert("names", text(r#"["a","b"]"#)),
|
||||
json!(["a", "b"])
|
||||
);
|
||||
assert_eq!(params.convert("nothing", text("null")), json!(null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -333,7 +513,22 @@ mod tests {
|
||||
}
|
||||
|
||||
fn converted_number_text(params: &ToolSchema, value: &str) -> String {
|
||||
serde_json::to_string(¶ms.convert("value", value)).unwrap()
|
||||
serde_json::to_string(¶ms.convert("value", text(value))).unwrap()
|
||||
}
|
||||
|
||||
fn text(value: &str) -> ParamInput {
|
||||
ParamInput::Text(value.to_string())
|
||||
}
|
||||
|
||||
fn elem(name: &str, value: ParamInput) -> ParamElement {
|
||||
ParamElement {
|
||||
name: name.to_string(),
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(elements: Vec<ParamElement>) -> ParamInput {
|
||||
ParamInput::Elements(elements)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -350,12 +545,12 @@ mod tests {
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(params.convert("s", "x"), json!("x"));
|
||||
assert_eq!(params.convert("i", "7"), json!(7));
|
||||
assert_eq!(params.convert("n", "7.5"), json!(7.5));
|
||||
assert_eq!(params.convert("b", "true"), json!(true));
|
||||
assert_eq!(params.convert("a", "[1]"), json!([1]));
|
||||
assert_eq!(params.convert("o", r#"{"x":1}"#), json!({ "x": 1 }));
|
||||
assert_eq!(params.convert("s", text("x")), json!("x"));
|
||||
assert_eq!(params.convert("i", text("7")), json!(7));
|
||||
assert_eq!(params.convert("n", text("7.5")), json!(7.5));
|
||||
assert_eq!(params.convert("b", text("true")), json!(true));
|
||||
assert_eq!(params.convert("a", text("[1]")), json!([1]));
|
||||
assert_eq!(params.convert("o", text(r#"{"x":1}"#)), json!({ "x": 1 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -373,8 +568,8 @@ mod tests {
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(integer_first.convert("value", "42"), json!(42));
|
||||
assert_eq!(string_first.convert("value", "42"), json!("42"));
|
||||
assert_eq!(integer_first.convert("value", text("42")), json!(42));
|
||||
assert_eq!(string_first.convert("value", text("42")), json!("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -396,9 +591,9 @@ mod tests {
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(params.convert("choice", "42"), json!(42));
|
||||
assert_eq!(params.convert("choice", text("42")), json!(42));
|
||||
assert_eq!(
|
||||
params.convert("fallback_object", r#"{"x":1}"#),
|
||||
params.convert("fallback_object", text(r#"{"x":1}"#)),
|
||||
json!({ "x": 1 })
|
||||
);
|
||||
}
|
||||
@@ -414,9 +609,12 @@ mod tests {
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(params.convert("choice", "a"), json!("a"));
|
||||
assert_eq!(params.convert("items", "[1,2]"), json!([1, 2]));
|
||||
assert_eq!(params.convert("payload", r#"{"x":1}"#), json!({ "x": 1 }));
|
||||
assert_eq!(params.convert("choice", text("a")), json!("a"));
|
||||
assert_eq!(params.convert("items", text("[1,2]")), json!([1, 2]));
|
||||
assert_eq!(
|
||||
params.convert("payload", text(r#"{"x":1}"#)),
|
||||
json!({ "x": 1 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -518,4 +716,162 @@ mod tests {
|
||||
assert_eq!(converted.get("topn"), Some(&json!("5")));
|
||||
assert_eq!(converted.get("nullish"), Some(&json!(null)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_structured_inputs_with_recursive_schema() {
|
||||
let schemas = ToolSchemas::from_tools(&[test_tool(
|
||||
"create_order",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": { "type": "integer" },
|
||||
"urgent": { "type": "boolean" },
|
||||
"note": { "type": "string" },
|
||||
"nil": { "type": "string" },
|
||||
"shipping": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": { "type": "string" },
|
||||
"zip": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sku": { "type": "string" },
|
||||
"qty": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "integer" }
|
||||
},
|
||||
"duplicate_demo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"schema_mismatch_array": {
|
||||
"type": "array",
|
||||
"items": { "type": "integer" }
|
||||
},
|
||||
"closed_object": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"open_object": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"payload_text": { "type": "object" },
|
||||
"items_text": { "type": "array" }
|
||||
}
|
||||
}),
|
||||
)]);
|
||||
|
||||
let converted = schemas.convert_params_with_schema(
|
||||
"create_order",
|
||||
vec![
|
||||
("user_id".to_string(), text("42")),
|
||||
("urgent".to_string(), text("true")),
|
||||
("note".to_string(), text("Please leave at front desk.")),
|
||||
("nil".to_string(), text("NULL")),
|
||||
(
|
||||
"shipping".to_string(),
|
||||
elements(vec![
|
||||
elem("city", text("Singapore")),
|
||||
elem("zip", text("018956")),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"items".to_string(),
|
||||
elements(vec![
|
||||
elem(
|
||||
"item1",
|
||||
elements(vec![elem("sku", text("book-001")), elem("qty", text("2"))]),
|
||||
),
|
||||
elem(
|
||||
"item2",
|
||||
elements(vec![elem("sku", text("pen-007")), elem("qty", text("5"))]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"metadata".to_string(),
|
||||
elements(vec![elem("score", text("42")), elem("rank", text("7"))]),
|
||||
),
|
||||
(
|
||||
"duplicate_demo".to_string(),
|
||||
elements(vec![elem("tag", text("a")), elem("tag", text("b"))]),
|
||||
),
|
||||
(
|
||||
"closed_object".to_string(),
|
||||
elements(vec![elem("unknown", text("x"))]),
|
||||
),
|
||||
(
|
||||
"open_object".to_string(),
|
||||
elements(vec![elem("unknown", text("y"))]),
|
||||
),
|
||||
("payload_text".to_string(), text(r#"{"x":1}"#)),
|
||||
("items_text".to_string(), text("[1,2]")),
|
||||
(
|
||||
"unknown_struct".to_string(),
|
||||
elements(vec![
|
||||
elem("a", text("1")),
|
||||
elem("a", text("2")),
|
||||
elem("nil", text("null")),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Value::Object(converted),
|
||||
json!({
|
||||
"user_id": 42,
|
||||
"urgent": true,
|
||||
"note": "Please leave at front desk.",
|
||||
"nil": null,
|
||||
"shipping": {
|
||||
"city": "Singapore",
|
||||
"zip": 18956
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"sku": "book-001",
|
||||
"qty": 2
|
||||
},
|
||||
{
|
||||
"sku": "pen-007",
|
||||
"qty": 5
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"score": 42,
|
||||
"rank": 7
|
||||
},
|
||||
"duplicate_demo": {
|
||||
"tag": ["a", "b"]
|
||||
},
|
||||
"closed_object": {
|
||||
"unknown": "x"
|
||||
},
|
||||
"open_object": {
|
||||
"unknown": "y"
|
||||
},
|
||||
"payload_text": {
|
||||
"x": 1
|
||||
},
|
||||
"items_text": [1, 2],
|
||||
"unknown_struct": {
|
||||
"a": ["1", "2"],
|
||||
"nil": null
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user