I Built the Missing Guardrails Package for Dart

2026-11-07来源 dev.toEnglish

I shipped a Flutter app to production last year with an LLM chatbot baked in. Two weeks later a user pasted their Aadhaar number into the chat, the model echoed it back, and I spent a weekend writing a PII scrubber by hand. That scrubber grew into ai_guardrails.

Python has NeMo Guardrails, Guardrails AI, LLM Guard. Dart had nothing. If you're building AI features in Flutter or server-side Dart, you've been on your own for input/output safety. This package closes that gap: 35 scanners, 893 tests, zero runtime dependencies, runs anywhere Dart runs.

What it actually does

ai_guardrails sits between your user's text and your LLM call. Input scanners run first: they can redact PII, block prompt injection attempts, catch leaked secrets. Then your LLM runs. Output scanners validate what comes back: JSON schema conformance, hallucination flags, toxicity scores, reading level checks. The pipeline stops at the first scanner that blocks.

import 'package:ai_guardrails/ai_guardrails.dart'; final guard = AiGuard( inputScanners: [ PiiScanner(action: GuardAction.redact), PromptInjectionScanner(), SecretScanner(), ], outputScanners: [ JsonValidator(schema: mySchema), ToxicityScanner(), ], ); final outcome = await guard.run( userInput, llm: (sanitised) => callYourModel(sanitised), ); if (outcome.blocked) { print('Blocked at ${outcome.blockedStage}: ${outcome.blockReason}'); } else { // outcome.output has PII rehydrated back to original values print(outcome.output); } 

The PiiScanner replaces jane.doe@example.com with [EMAIL_1] on the way in, your model sees the placeholder, and outcome.output swaps the original email back in on the way out. The model never sees the real data. That round-trip was the part I couldn't find a clean solution for in any existing package.

PII coverage is the hard part

Email and SSN regex is table stakes. The part that took actual work: 9 locale-specific PII patterns. Brazil CPF/CNPJ, Mexico CURP/RFC, Japan My Number, South Korea RRN, Canada SIN with Luhn validation, Australia TFN and Medicare, plus EU and India phone formats. The Canadian SIN scanner rejects Luhn-invalid numbers so you don't flag random 9-digit strings in normal text.

RTL text works too. An email buried in Hebrew or a credit card in Arabic bidi text gets caught and redacted with the surrounding script structure intact. I didn't anticipate needing that until a tester pasted mixed-direction text and the redaction offsets went sideways.

35 scanners, no ML

Every scanner is pure heuristic. No ONNX, no dart:ffi, no model downloads. The full list:

Input safety: PromptInjectionScanner, SemanticInjectionScanner, InvisibleTextScanner, PaddingAttackScanner, SecretScanner, PiiScanner, BannedPatternScanner, BannedTopicScanner, TokenLimitScanner, LanguageScanner, LanguageDetectionScanner, ToxicityScanner

Output validation: JsonValidator, SchemaValidator, SqlValidator, HtmlValidator, UrlFormatValidator, RangeValidator, ChoicesValidator, ReadingLevelScanner, RepetitionScanner, RelevanceScanner, PolitenessScanner, BiasScanner

Grounding and RAG: GroundingScanner, EmbeddingGroundingScanner, HallucinationScanner, FactCheckScanner

Agentic: ToolCallScanner, ToolOutputScanner, CodeExecutionScanner, UrlScanner, CompetitorMentionScanner, TopicSafetyScanner, TopicAllowlistScanner

The LanguageDetectionScanner is one I'm particularly happy with. It uses trigram frequency profiles for 8 Latin-script languages (English, Spanish, French, German, Italian, Portuguese, Dutch, Turkish) and Unicode range detection for CJK, Devanagari, Cyrillic, and Hangul. Not as accurate as fastText, but it runs in under 50µs with no model file, and for a guardrail ("is this input in an expected language?") that's more than enough.

Streaming

StreamingAiGuard wraps a Stream<String> and applies output scanners to an accumulating buffer. When a scanner blocks mid-stream, it cancels the subscription and reports what triggered it. You don't have to buffer the entire response yourself.

final streamGuard = StreamingAiGuard(outputScanners: [ToxicityScanner()]); final result = await streamGuard.guardStream(myLlmStream); 

Provider wrappers

The core package is provider-agnostic. Wrapper packages handle the wiring for specific SDKs:

  • ai_guardrails_google for Google AI (Gemini)
  • ai_guardrails_anthropic for Claude
  • ai_guardrails_langchain for LangChain.dart (a GuardrailsChain that wraps any BaseChain, and a GuardrailsTool for agent pipelines)
  • ai_guardrails_perspective for Google Perspective API
  • ai_guardrails_server for shelf/dart_frog middleware

The LangChain integration was the most recent addition. GuardrailsChain wraps your inner chain with input/output scanning and carries a PII redaction map through the chain's output keys so downstream chains can reference what was redacted.

final chain = GuardrailsChain( innerChain: myLlmChain, guard: AiGuard( inputScanners: [PiiScanner(action: GuardAction.redact)], outputScanners: [ToxicityScanner()], ), ); 

Why on-device matters

Two reasons this is pure heuristic and not an API call to a moderation endpoint.

First: GDPR and the EU AI Act. If you're redacting PII to protect users, sending that PII to a cloud moderation service to decide whether to redact it defeats the purpose. The redaction has to happen before the text leaves the device.

Second: cost. A moderation API call per message adds up. Regex and trigram matching costs you a few hundred microseconds of CPU time and zero dollars.

The trade-off is accuracy. A keyword-based ToxicityScanner won't catch subtle sarcasm or context-dependent toxicity. I haven't tested it against adversarial jailbreak datasets. For the use case this targets (catching obvious violations, redacting structured PII, enforcing output format), heuristics work. If you need ML-grade content moderation, pair this with the ai_guardrails_perspective wrapper and use both.

Getting started

dart pub add ai_guardrails 

Dart SDK ^3.5.0, which means Flutter 3.24+. Zero dependencies to install.

The example app has a chat screen with toggle chips for each scanner so you can see what each one catches in real time. The API docs cover every scanner's parameters.

If you're building AI features in Dart and you've been writing your own validation, I'd like to hear what patterns you've needed. The scanner interface is simple enough to extend (implement Scanner, return a ScanResult), and the registry lets you configure scanners from JSON, so plugging new ones in is a few lines.

Repo: github.com/GhagSagar23/ai_guardrails
pub.dev: pub.dev/packages/ai_guardrails

原文:https://www.a8q9.cn/en/article/i-built-the-missing-guardrails-package-for-dart-ba6661.html

相关文章