use async_trait::async_trait;
use thiserror::Error;
use crate::run::AgentRunState;
use super::{OutputSchema, OutputValue};
#[derive(Debug, Error)]
pub enum OutputValidationError {
#[error("output is not valid JSON: {0}")]
InvalidJson(String),
#[error("output schema validation failed: {0}")]
Schema(String),
#[error("output retry requested: {0}")]
Retry(String),
#[error("output validation failed: {0}")]
Failed(String),
}
impl OutputValidationError {
#[must_use]
pub fn retry(message: impl Into<String>) -> Self {
Self::Retry(message.into())
}
#[must_use]
pub fn failed(message: impl Into<String>) -> Self {
Self::Failed(message.into())
}
}
pub type OutputValidationResult<T> = Result<T, OutputValidationError>;
#[async_trait]
pub trait OutputValidator: Send + Sync {
async fn validate(
&self,
state: &mut AgentRunState,
output: &OutputValue,
) -> OutputValidationResult<()>;
}
pub struct FunctionOutputValidator<F> {
function: F,
}
impl<F> FunctionOutputValidator<F> {
#[must_use]
pub const fn new(function: F) -> Self {
Self { function }
}
}
#[async_trait]
impl<F, Fut> OutputValidator for FunctionOutputValidator<F>
where
F: Send + Sync + Fn(&mut AgentRunState, &OutputValue) -> Fut,
Fut: Send + std::future::Future<Output = OutputValidationResult<()>>,
{
async fn validate(
&self,
state: &mut AgentRunState,
output: &OutputValue,
) -> OutputValidationResult<()> {
(self.function)(state, output).await
}
}
pub fn parse_output(
raw_output: &str,
schema: Option<&OutputSchema>,
) -> OutputValidationResult<OutputValue> {
match schema {
Some(schema) => {
let value = serde_json::from_str(raw_output)
.map_err(|error| OutputValidationError::InvalidJson(error.to_string()))?;
validate_json_value(&value, schema)?;
Ok(OutputValue::Json(value))
}
None => Ok(OutputValue::Text(raw_output.to_string())),
}
}
fn validate_json_value(
value: &serde_json::Value,
schema: &OutputSchema,
) -> OutputValidationResult<()> {
let validator = jsonschema::validator_for(&schema.schema).map_err(|error| {
OutputValidationError::Schema(format!("invalid output schema {}: {error}", schema.name))
})?;
validator.validate(value).map_err(|error| {
let path = error.instance_path.as_str();
let detail = error.masked().to_string();
if path.is_empty() {
OutputValidationError::Schema(detail)
} else {
OutputValidationError::Schema(format!("{path}: {detail}"))
}
})
}