use super::guardrail::{GuardrailError, GuardrailResult, GuardrailsConfig};
#[derive(Debug, Clone)]
pub struct GuardrailViolation {
pub guardrail_name: String,
pub stage: String,
pub reason: String,
}
pub struct GuardrailRunner {
config: GuardrailsConfig,
violations: Vec<GuardrailViolation>,
}
impl GuardrailRunner {
pub fn new(config: GuardrailsConfig) -> Self {
Self {
config,
violations: Vec::new(),
}
}
pub async fn validate_input(&mut self, input: &str) -> Result<(), GuardrailError> {
let mut first_block: Option<String> = None;
for g in &self.config.input_guardrails {
if let GuardrailResult::Block { reason } = g.validate(input).await {
self.violations.push(GuardrailViolation {
guardrail_name: g.name().to_string(),
stage: "input".to_string(),
reason: reason.clone(),
});
if first_block.is_none() {
first_block = Some(reason);
}
if self.config.fail_fast {
return Err(GuardrailError::Blocked(first_block.unwrap()));
}
}
}
if let Some(r) = first_block {
return Err(GuardrailError::Blocked(r));
}
Ok(())
}
pub async fn validate_output(&mut self, output: &str) -> Result<String, GuardrailError> {
let mut current = output.to_string();
let mut first_block: Option<String> = None;
for g in &self.config.output_guardrails {
match g.validate(¤t).await {
GuardrailResult::Pass => {}
GuardrailResult::Block { reason } => {
self.violations.push(GuardrailViolation {
guardrail_name: g.name().to_string(),
stage: "output".to_string(),
reason: reason.clone(),
});
if first_block.is_none() {
first_block = Some(reason);
}
if self.config.fail_fast {
return Err(GuardrailError::Blocked(first_block.unwrap()));
}
}
GuardrailResult::Modify { new_value } => {
current = new_value;
}
}
}
if let Some(r) = first_block {
return Err(GuardrailError::Blocked(r));
}
Ok(current)
}
pub fn violations(&self) -> &[GuardrailViolation] {
&self.violations
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::guardrails::guardrail::InputGuardrail;
use async_trait::async_trait;
use std::sync::Arc;
struct AlwaysBlock;
#[async_trait]
impl InputGuardrail for AlwaysBlock {
fn name(&self) -> &str {
"AlwaysBlock"
}
async fn validate(&self, _input: &str) -> GuardrailResult {
GuardrailResult::Block {
reason: "always".to_string(),
}
}
}
struct AlwaysPass;
#[async_trait]
impl InputGuardrail for AlwaysPass {
fn name(&self) -> &str {
"AlwaysPass"
}
async fn validate(&self, _input: &str) -> GuardrailResult {
GuardrailResult::Pass
}
}
#[tokio::test]
async fn test_runner_pass() {
let config = GuardrailsConfig::new().with_input(Arc::new(AlwaysPass));
let mut runner = GuardrailRunner::new(config);
assert!(runner.validate_input("hi").await.is_ok());
}
#[tokio::test]
async fn test_runner_block() {
let config = GuardrailsConfig::new().with_input(Arc::new(AlwaysBlock));
let mut runner = GuardrailRunner::new(config);
assert!(runner.validate_input("hi").await.is_err());
assert_eq!(runner.violations().len(), 1);
}
#[tokio::test]
async fn test_runner_fail_fast_collects_one() {
let config = GuardrailsConfig::new()
.with_input(Arc::new(AlwaysBlock))
.with_input(Arc::new(AlwaysBlock))
.fail_fast(true);
let mut runner = GuardrailRunner::new(config);
let _ = runner.validate_input("hi").await;
assert_eq!(runner.violations().len(), 1);
}
#[tokio::test]
async fn test_runner_no_fail_fast_collects_all() {
let config = GuardrailsConfig::new()
.with_input(Arc::new(AlwaysBlock))
.with_input(Arc::new(AlwaysBlock))
.fail_fast(false);
let mut runner = GuardrailRunner::new(config);
let _ = runner.validate_input("hi").await;
assert_eq!(runner.violations().len(), 2);
}
}