use anyhow::Result;use schemars::{ JsonSchema, Schema, generate::SchemaSettings, transform::{Transform, transform_subschemas},};use serde_json::Value; /// Indicates the format used to define the input schema for a language model tool.#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]pub enum LanguageModelToolSchemaFormat { /// A JSON schema, see https://json-schema.org JsonSchema, /// A subset of an OpenAPI 3.0 schema object supported by Google AI, see https://ai.google.dev/api/caching#Schema JsonSchemaSubset,} pub fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema { let mut generator = match format { LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07() .with(|settings| { settings.meta_schema = None; settings.inline_subschemas = true; }) .into_generator(), LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() .with(|settings| { settings.meta_schema = None; settings.inline_subschemas = true; }) .with_transform(ToJsonSchemaSubsetTransform) .into_generator(), }; generator.root_schema_for::<T>()} #[derive(Debug, Clone)]struct ToJsonSchemaSubsetTransform; impl Transform for ToJsonSchemaSubsetTransform { fn transform(&mut self, schema: &mut Schema) { // Ensure that the type field is not an array, this happens when we use // Option<T>, the type will be [T, "null"]. if let Some(type_field) = schema.get_mut("type") && let Some(types) = type_field.as_array() && let Some(first_type) = types.first() { *type_field = first_type.clone(); } // oneOf is not supported, use anyOf instead if let Some(one_of) = schema.remove("oneOf") { schema.insert("anyOf".to_string(), one_of); } transform_subschemas(self, schema); }} /// Tries to adapt a JSON schema representation to be compatible with the specified format.////// If the json cannot be made compatible with the specified format, an error is returned.pub fn adapt_schema_to_format( json: &mut Value, format: LanguageModelToolSchemaFormat,) -> Result<()> { if let Value::Object(obj) = json { obj.remove("$schema"); obj.remove("title"); obj.remove("description"); } match format { LanguageModelToolSchemaFormat::JsonSchema => preprocess_json_schema(json), LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json), }} fn preprocess_json_schema(json: &mut Value) -> Result<()> { if let Value::Object(obj) = json && matches!(obj.get("type"), Some(Value::String(s)) if s == "object") { if !obj.contains_key("additionalProperties") { obj.insert("additionalProperties".to_string(), Value::Bool(false)); } if !obj.contains_key("properties") { obj.insert("properties".to_string(), Value::Object(Default::default())); } } Ok(())} fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> { if let Value::Object(obj) = json { const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"]; for key in UNSUPPORTED_KEYS { anyhow::ensure!( !obj.contains_key(key), "Schema cannot be made compatible because it contains \"{key}\"" ); } const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 6] = [ ("format", |value| value.is_string()), ("additionalProperties", |_| true), ("propertyNames", |_| true), ("exclusiveMinimum", |value| value.is_number()), ("exclusiveMaximum", |value| value.is_number()), ("optional", |value| value.is_boolean()), ]; for (key, predicate) in KEYS_TO_REMOVE { if let Some(value) = obj.get(key) && predicate(value) { obj.remove(key); } } if matches!(obj.get("description"), Some(Value::String(_))) && !obj.contains_key("type") && !(obj.contains_key("anyOf") || obj.contains_key("oneOf") || obj.contains_key("allOf")) { obj.insert("type".to_string(), Value::String("string".to_string())); } if let Some(subschemas) = obj.get_mut("oneOf") && subschemas.is_array() { let subschemas_clone = subschemas.clone(); obj.remove("oneOf"); obj.insert("anyOf".to_string(), subschemas_clone); } for (_, value) in obj.iter_mut() { if let Value::Object(_) | Value::Array(_) = value { adapt_to_json_schema_subset(value)?; } } } else if let Value::Array(arr) = json { for item in arr.iter_mut() { adapt_to_json_schema_subset(item)?; } } Ok(())} #[cfg(test)]mod tests { use super::*; use serde_json::json; #[test] fn test_transform_adds_type_when_missing() { let mut json = json!({ "description": "A test field without type" }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "description": "A test field without type", "type": "string" }) ); let mut json = json!({ "description": { "value": "abc", "type": "string" } }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "description": { "value": "abc", "type": "string" } }) ); } #[test] fn test_transform_removes_unsupported_keys() { let mut json = json!({ "description": "A test field", "type": "integer", "format": "uint32", "exclusiveMinimum": 0, "exclusiveMaximum": 100, "additionalProperties": false, "optional": true }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "description": "A test field", "type": "integer" }) ); let mut json = json!({ "description": "A test field", "type": "integer", "format": {}, }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "description": "A test field", "type": "integer", "format": {}, }) ); let mut json = json!({ "type": "object", "properties": { "name": { "type": "string" } }, "additionalProperties": { "type": "string" }, "propertyNames": { "pattern": "^[A-Za-z]+$" } }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "type": "object", "properties": { "name": { "type": "string" } } }) ); } #[test] fn test_transform_one_of_to_any_of() { let mut json = json!({ "description": "A test field", "oneOf": [ { "type": "string" }, { "type": "integer" } ] }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "description": "A test field", "anyOf": [ { "type": "string" }, { "type": "integer" } ] }) ); } #[test] fn test_transform_nested_objects() { let mut json = json!({ "type": "object", "properties": { "nested": { "oneOf": [ { "type": "string" }, { "type": "null" } ], "format": "email" } } }); adapt_to_json_schema_subset(&mut json).unwrap(); assert_eq!( json, json!({ "type": "object", "properties": { "nested": { "anyOf": [ { "type": "string" }, { "type": "null" } ] } } }) ); } #[test] fn test_transform_fails_if_unsupported_keys_exist() { let mut json = json!({ "type": "object", "properties": { "$ref": "#/definitions/User", } }); assert!(adapt_to_json_schema_subset(&mut json).is_err()); let mut json = json!({ "type": "object", "properties": { "if": "...", } }); assert!(adapt_to_json_schema_subset(&mut json).is_err()); let mut json = json!({ "type": "object", "properties": { "then": "...", } }); assert!(adapt_to_json_schema_subset(&mut json).is_err()); let mut json = json!({ "type": "object", "properties": { "else": "...", } }); assert!(adapt_to_json_schema_subset(&mut json).is_err()); } #[test] fn test_preprocess_json_schema_adds_additional_properties() { let mut json = json!({ "type": "object", "properties": { "name": { "type": "string" } } }); preprocess_json_schema(&mut json).unwrap(); assert_eq!( json, json!({ "type": "object", "properties": { "name": { "type": "string" } }, "additionalProperties": false }) ); } #[test] fn test_preprocess_json_schema_preserves_additional_properties() { let mut json = json!({ "type": "object", "properties": { "name": { "type": "string" } }, "additionalProperties": true }); preprocess_json_schema(&mut json).unwrap(); assert_eq!( json, json!({ "type": "object", "properties": { "name": { "type": "string" } }, "additionalProperties": true }) ); }}
use anyhow::Result;
use schemars::{
JsonSchema, Schema,
generate::SchemaSettings,
transform::{Transform, transform_subschemas},
};
use serde_json::Value;
/// Indicates the format used to define the input schema for a language model tool.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum LanguageModelToolSchemaFormat {
/// A JSON schema, see https://json-schema.org
JsonSchema,
/// A subset of an OpenAPI 3.0 schema object supported by Google AI, see https://ai.google.dev/api/caching#Schema
JsonSchemaSubset,
}
pub fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
})
.into_generator(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
})
.with_transform(ToJsonSchemaSubsetTransform)
.into_generator(),
};
generator.root_schema_for::<T>()
}
#[derive(Debug, Clone)]
struct ToJsonSchemaSubsetTransform;
impl Transform for ToJsonSchemaSubsetTransform {
fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
if let Some(type_field) = schema.get_mut("type")
&& let Some(types) = type_field.as_array()
&& let Some(first_type) = types.first()
{
*type_field = first_type.clone();
}
// oneOf is not supported, use anyOf instead
if let Some(one_of) = schema.remove("oneOf") {
schema.insert("anyOf".to_string(), one_of);
}
transform_subschemas(self, schema);
}
}
/// Tries to adapt a JSON schema representation to be compatible with the specified format.
///
/// If the json cannot be made compatible with the specified format, an error is returned.
pub fn adapt_schema_to_format(
json: &mut Value,
format: LanguageModelToolSchemaFormat,
) -> Result<()> {
if let Value::Object(obj) = json {
obj.remove("$schema");
obj.remove("title");
obj.remove("description");
}
match format {
LanguageModelToolSchemaFormat::JsonSchema => preprocess_json_schema(json),
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
}
}
fn preprocess_json_schema(json: &mut Value) -> Result<()> {
if let Value::Object(obj) = json
&& matches!(obj.get("type"), Some(Value::String(s)) if s == "object")
{
if !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".to_string(), Value::Bool(false));
}
if !obj.contains_key("properties") {
obj.insert("properties".to_string(), Value::Object(Default::default()));
}
}
Ok(())
}
fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
if let Value::Object(obj) = json {
const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
for key in UNSUPPORTED_KEYS {
anyhow::ensure!(
!obj.contains_key(key),
"Schema cannot be made compatible because it contains \"{key}\""
);
}
const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 6] = [
("format", |value| value.is_string()),
("additionalProperties", |_| true),
("propertyNames", |_| true),
("exclusiveMinimum", |value| value.is_number()),
("exclusiveMaximum", |value| value.is_number()),
("optional", |value| value.is_boolean()),
];
for (key, predicate) in KEYS_TO_REMOVE {
if let Some(value) = obj.get(key)
&& predicate(value)
{
obj.remove(key);
}
}
if matches!(obj.get("description"), Some(Value::String(_)))
&& !obj.contains_key("type")
&& !(obj.contains_key("anyOf")
|| obj.contains_key("oneOf")
|| obj.contains_key("allOf"))
{
obj.insert("type".to_string(), Value::String("string".to_string()));
}
if let Some(subschemas) = obj.get_mut("oneOf")
&& subschemas.is_array()
{
let subschemas_clone = subschemas.clone();
obj.remove("oneOf");
obj.insert("anyOf".to_string(), subschemas_clone);
}
for (_, value) in obj.iter_mut() {
if let Value::Object(_) | Value::Array(_) = value {
adapt_to_json_schema_subset(value)?;
}
}
} else if let Value::Array(arr) = json {
for item in arr.iter_mut() {
adapt_to_json_schema_subset(item)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_transform_adds_type_when_missing() {
let mut json = json!({
"description": "A test field without type"
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field without type",
"type": "string"
})
);
let mut json = json!({
"description": {
"value": "abc",
"type": "string"
}
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": {
"value": "abc",
"type": "string"
}
})
);
}
#[test]
fn test_transform_removes_unsupported_keys() {
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": "uint32",
"exclusiveMinimum": 0,
"exclusiveMaximum": 100,
"additionalProperties": false,
"optional": true
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "integer"
})
);
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": {},
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "integer",
"format": {},
})
);
let mut json = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"additionalProperties": { "type": "string" },
"propertyNames": { "pattern": "^[A-Za-z]+$" }
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
})
);
}
#[test]
fn test_transform_one_of_to_any_of() {
let mut json = json!({
"description": "A test field",
"oneOf": [
{ "type": "string" },
{ "type": "integer" }
]
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"anyOf": [
{ "type": "string" },
{ "type": "integer" }
]
})
);
}
#[test]
fn test_transform_nested_objects() {
let mut json = json!({
"type": "object",
"properties": {
"nested": {
"oneOf": [
{ "type": "string" },
{ "type": "null" }
],
"format": "email"
}
}
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"nested": {
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
}
})
);
}
#[test]
fn test_transform_fails_if_unsupported_keys_exist() {
let mut json = json!({
"type": "object",
"properties": {
"$ref": "#/definitions/User",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
let mut json = json!({
"type": "object",
"properties": {
"if": "...",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
let mut json = json!({
"type": "object",
"properties": {
"then": "...",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
let mut json = json!({
"type": "object",
"properties": {
"else": "...",
}
});
assert!(adapt_to_json_schema_subset(&mut json).is_err());
}
#[test]
fn test_preprocess_json_schema_adds_additional_properties() {
let mut json = json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
});
preprocess_json_schema(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
})
);
}
#[test]
fn test_preprocess_json_schema_preserves_additional_properties() {
let mut json = json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": true
});
preprocess_json_schema(&mut json).unwrap();
assert_eq!(
json,
json!({
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": true
})
);
}
}