use jsonschema::Validator;
use machi_types::{ErrorCode, MachiError};
use serde_json::Value;
pub const STRUCTURED_OUTPUT_MAX_RETRIES: u32 = 3;
pub fn compile_schema(schema: &Value) -> Result<Validator, MachiError> {
Validator::new(schema).map_err(|e| {
MachiError::new(
ErrorCode::RuntimeStructuredOutput,
format!("invalid output schema: {e}"),
)
})
}
pub fn validate_structured_output(validator: &Validator, raw: &str) -> Result<Value, String> {
let value: Value = serde_json::from_str(raw.trim())
.map_err(|e| format!("model output was not valid JSON: {e}"))?;
validator
.validate(&value)
.map_err(|e| format!("output does not match the required schema: {e}"))?;
Ok(value)
}
#[must_use]
pub fn schema_retry_reminder(error: &str) -> String {
format!(
"Your previous response failed structured-output validation:\n{error}\n\
Reply with JSON only that satisfies the required schema."
)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn accepts_valid() {
let schema = json!({
"type": "object",
"properties": { "ok": { "type": "boolean" } },
"required": ["ok"],
"additionalProperties": false
});
let v = compile_schema(&schema).expect("schema");
let out = validate_structured_output(&v, r#"{"ok": true}"#).expect("ok");
assert_eq!(out.get("ok").and_then(Value::as_bool), Some(true));
}
#[test]
fn rejects_invalid() {
let schema = json!({
"type": "object",
"properties": { "ok": { "type": "boolean" } },
"required": ["ok"]
});
let v = compile_schema(&schema).expect("schema");
let err = validate_structured_output(&v, r#"{"ok": "nope"}"#).expect_err("bad");
assert!(err.contains("schema") || err.contains("type"), "{err}");
}
}