Skip to content

Repository files navigation

guardflow

crates.io docs.rs CI license

Validators and a retry-until-valid guard for LLM output — the core pattern behind Python's Guardrails AI. No Rust equivalent as of 2026.

Install

cargo add guardflow

The pattern: format checks are free, real checks need a model call

Guard runs cheap sync checks (length, regex, PII shape, JSON schema). AsyncGuard adds checks that need a network call — an LLM-as-judge, a moderation API — and only makes that call once the free checks already pass:

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

let guard = AsyncGuard::from_guard(Guard::new().with(NoPii::default()))
    .with_async(CustomAsyncValidator::new("refund_policy", |text| async move {
        if text.to_lowercase().contains("guaranteed refund") {
            Err("promises a refund outside policy".into())
        } else {
            Ok(())
        }
    }));

let reply = guard_with_retry_async(&guard, 3, |last_failure| {
    let prompt = match last_failure {
        Some(f) => format!("Fix this: {:?}. Try again.", f.failures),
        None => "Reply to the customer.".to_string(),
    };
    call_llm(prompt) // returns a Future<Output = String>
}).await;

Run it: cargo run --example support_bot — a fake LLM leaks an email (rejected by the free no_pii check), then over-promises a refund (rejected by the async policy check, which only ran because the free check passed first), then succeeds on the third try.

Built-in validators

NotEmpty, MaxLength, MinLength, MatchesRegex, OneOf, ValidJson, NoPii (heuristic regex, not ML), Profanity (bring your own word list), JsonSchema (feature json-schema). Implement Validator (sync) or AsyncValidator for your own checks.

Auto-fix instead of reject: some validators can repair input instead of just failing it — Truncate shortens instead of rejecting on length. Guard::fix(text) applies every fixable validator in order and returns the corrected text; Guard::check never rewrites anything, so Truncate counts as a failure there.

Rule files

Load a rule set from YAML or JSON instead of Rust code (feature spec):

# rules.yaml
rules:
  - rule: not_empty
  - rule: max_length
    max: 500
  - rule: no_pii
let guard = guardflow::spec::guard_from_file("rules.yaml")?;

graph-flow integration

With the graphflow feature, GuardedTask<T> wraps any graph_flow::Task, retrying until its response passes:

use guardflow::graphflow::GuardedTask;
let guarded = GuardedTask::new(my_task, guard).with_max_attempts(3);

Works with graphflow-stream — wrap a task before passing it to spawn_task/spawn_graph.

Examples

cargo run --example support_bot   # sync + async guard, retry-until-valid, full story

Benchmarks

cargo bench (benches/overhead.rs):

Scenario Time
Guard::check, 1 validator ~27 ns
Guard::check, 5 validators (incl. NoPii) ~1.2 µs

License

MIT

About

Validators and a retry-until-valid guard for LLM output, in Rust

Topics

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages