guardflow 0.2.0

Validators and a retry-until-valid guard for LLM output, in Rust
Documentation
//! A customer-support bot's reply pipeline: base rules (not empty, under 500 chars,
//! no PII) loaded from `examples/rules/support_reply.yaml`, plus a policy check that
//! only a real model call could make — "did this reply promise a refund we don't
//! actually authorize?" — wired up as an async validator. `guard_with_retry_async`
//! regenerates until both pass, or gives up after 3 tries.

use guardflow::validators::NoPii;
use guardflow::{AsyncGuard, CustomAsyncValidator, Guard, guard_with_retry_async};

/// Stands in for a real moderation/LLM-as-judge call. A real one would hit an API;
/// this one just checks for a phrase a support bot shouldn't be saying unprompted.
async fn passes_refund_policy(text: String) -> Result<(), String> {
    if text.to_lowercase().contains("guaranteed refund") {
        Err("promises a refund outside policy; only Tier 2 support can authorize refunds".into())
    } else {
        Ok(())
    }
}

/// Stands in for a real LLM call. Attempt 1 leaks PII (rejected by the sync `no_pii`
/// check, so `refund_policy` never even runs); attempt 2 fixes that but over-promises
/// a refund (only caught by the async `refund_policy` check); attempt 3 is clean.
fn fake_llm_reply(attempt: usize) -> String {
    match attempt {
        1 => "Email us at support@ourcompany.com and we'll take a look.".to_string(),
        2 => "I can offer you a guaranteed refund right now, no questions asked.".to_string(),
        _ => "Thanks for reaching out — I've logged this and a specialist will follow up shortly."
            .to_string(),
    }
}

#[tokio::main]
async fn main() {
    let base = Guard::new().with(NoPii::default());
    // If you'd rather load the base rules from examples/rules/support_reply.yaml
    // instead of building them in code, swap the line above for:
    //   let base = guardflow::spec::guard_from_file("examples/rules/support_reply.yaml").unwrap();

    let guard = AsyncGuard::from_guard(base).with_async(CustomAsyncValidator::new(
        "refund_policy",
        passes_refund_policy,
    ));

    let mut attempt = 0;
    let result = guard_with_retry_async(&guard, 3, |last_failure| {
        attempt += 1;
        if let Some(failure) = last_failure {
            println!("attempt {attempt} rejected, retrying:");
            for f in &failure.failures {
                println!("  - {}: {}", f.validator, f.message);
            }
        }
        let reply = fake_llm_reply(attempt);
        async move { reply }
    })
    .await;

    match result {
        Ok(reply) => println!("\napproved reply (attempt {attempt}): {reply}"),
        Err(failure) => println!("\ngave up after {attempt} attempts: {failure:?}"),
    }
}