use serde_json::{json, Value};
use crate::error::Result;
use crate::types::Tool;
use super::definition_parts;
fn strictify(mut parameters: Value, want_strict: bool) -> (Value, bool) {
if !want_strict || !qualifies_for_strict(¶meters) {
return (parameters, false);
}
close_objects(&mut parameters);
(parameters, true)
}
fn qualifies_for_strict(v: &Value) -> bool {
match v {
Value::Object(map) => {
if map.get("type").and_then(Value::as_str) == Some("object") {
let required: Vec<&str> = map
.get("required")
.and_then(Value::as_array)
.map(|r| r.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let all_required = map
.get("properties")
.and_then(Value::as_object)
.map(|p| p.keys().all(|k| required.contains(&k.as_str())))
.unwrap_or(true);
if !all_required {
return false;
}
}
map.values().all(qualifies_for_strict)
}
Value::Array(items) => items.iter().all(qualifies_for_strict),
_ => true,
}
}
fn close_objects(v: &mut Value) {
match v {
Value::Object(map) => {
if map.get("type").and_then(Value::as_str) == Some("object") {
map.insert("additionalProperties".into(), Value::Bool(false));
}
for (_, child) in map.iter_mut() {
close_objects(child);
}
}
Value::Array(items) => items.iter_mut().for_each(close_objects),
_ => {}
}
}
pub fn render_tools(action: &Tool) -> Result<Value> {
let (name, description, parameters) = definition_parts(action)?;
let (parameters, strict) = strictify(parameters, action.strict.unwrap_or(true));
Ok(json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
"strict": strict,
}
}))
}
pub fn render_responses(action: &Tool) -> Result<Value> {
let (name, description, parameters) = definition_parts(action)?;
let (parameters, strict) = strictify(parameters, action.strict.unwrap_or(true));
Ok(json!({
"type": "function",
"name": name,
"description": description,
"parameters": parameters,
"strict": strict,
}))
}
#[cfg(test)]
mod tests {
use super::super::tests::sample_def;
use super::*;
#[test]
fn tools_shape_has_nested_function_block() {
let v = render_tools(&sample_def()).unwrap();
assert_eq!(v["type"], "function");
assert_eq!(v["function"]["name"], "slack_post_message");
assert_eq!(v["function"]["parameters"]["type"], "object");
assert_eq!(v["function"]["strict"], true);
}
#[test]
fn a_strict_function_closes_every_object_in_its_schema() {
let mut a = sample_def();
a.strict = Some(true);
a.input_schema = Some(json!({
"type": "object",
"properties": {
"channel": {"type": "string"},
"opts": {
"type": "object",
"properties": {"pin": {"type": "boolean"}},
"required": ["pin"]
}
},
"required": ["channel", "opts"]
}));
let v = render_tools(&a).unwrap();
let p = &v["function"]["parameters"];
assert_eq!(v["function"]["strict"], true);
assert_eq!(p["additionalProperties"], false, "top level closed: {p}");
assert_eq!(
p["properties"]["opts"]["additionalProperties"], false,
"nested objects closed too: {p}"
);
}
#[test]
fn an_optional_property_downgrades_the_claim_rather_than_the_schema() {
let mut a = sample_def();
a.strict = Some(true);
a.input_schema = Some(json!({
"type": "object",
"properties": {"channel": {"type": "string"}, "thread": {"type": "string"}},
"required": ["channel"]
}));
let v = render_tools(&a).unwrap();
assert_eq!(
v["function"]["strict"], false,
"cannot honestly claim strict with an optional property"
);
assert_eq!(
v["function"]["parameters"]["required"],
json!(["channel"]),
"and the author's contract is left exactly as written"
);
assert!(
v["function"]["parameters"].get("additionalProperties").is_none(),
"including no closing we were not entitled to add: {}",
v["function"]["parameters"]
);
}
#[test]
fn a_no_argument_tool_renders_strict_and_closed() {
let mut a = sample_def();
a.strict = None; a.input_schema = Some(json!({"type": "object", "properties": {}}));
let v = render_tools(&a).unwrap();
assert_eq!(v["function"]["strict"], true);
assert_eq!(v["function"]["parameters"]["additionalProperties"], false);
}
#[test]
fn strict_false_leaves_the_schema_alone() {
let mut a = sample_def();
a.strict = Some(false);
a.input_schema = Some(json!({"type": "object", "properties": {}}));
let v = render_tools(&a).unwrap();
assert_eq!(v["function"]["strict"], false);
assert!(
v["function"]["parameters"].get("additionalProperties").is_none(),
"untouched: {}",
v["function"]["parameters"]
);
}
#[test]
fn responses_shape_is_flat() {
let v = render_responses(&sample_def()).unwrap();
assert_eq!(v["type"], "function");
assert_eq!(v["name"], "slack_post_message");
assert!(v.get("function").is_none(), "responses API flattens");
}
}