Build a Guarded AI Chatbot

Add input/output guardrails to a Gemini chatbot to block PII and filter content.

Introduction

30 Second Summary

AI chatbots are powerful, but they can also be unpredictable. Left unguarded, they might leak a user's private information, recommend a competitor's product, or veer into topics they have no business discussing. In production, that kind of behavior is a liability.

In this project, you will build a guarded AI chatbot protected by layered safety validators that block sensitive data, filter unwanted content, and keep responses on topic.

What You'll Build

A Google Colab notebook that wraps Google Gemini with Guardrails AI validators to create a chatbot protected by input and output guardrails.

By the end of this project, you'll have:

  • Input guardrails that detect and block PII before it ever reaches the LLM.
  • Output guardrails that filter competitor mentions from responses and enforce response length constraints.
  • A custom validator you write from scratch to enforce topic boundaries on LLM output.
  • Secret Mission: Add LLM-based toxic language detection that strips harmful content from responses.

Are there any prerequisites?

You'll need basic to intermediate Python knowledge and a Google account. No prior experience with guardrails or AI safety tooling is required.

Set Up Your Environment and API Keys

Before you can build guardrails around an AI chatbot, you need the right tools in place. Guardrails AI is a Python framework that lets you wrap validators around LLM calls to enforce safety rules on both inputs and outputs.

In this step, you will install Guardrails AI, connect it to the Guardrails Hub (a library of pre-built validators), and get a free Google Gemini API key so your chatbot has an LLM to talk to.

In this step, get ready to:
  • Install Guardrails AI and configure the Hub CLI with your free API key.
  • Get a free Google Gemini API key from Google AI Studio.
  • Verify the full setup works by calling Gemini through a basic Guard.
Install Guardrails AI and configure the Hub CLI

Guardrails AI ships as a pip package. The Hub CLI connects to the Guardrails Hub, where pre-built validators live as installable plugins.

  • Install Guardrails AI by running this command in a new Google Colab cell:
!pip install guardrails-ai

What does this command do?

The ! prefix tells Colab to run a shell command inside your notebook cell. pip install guardrails-ai downloads the Guardrails AI package and its dependencies into your Colab environment.

  • Wait for the installation to finish. You should see a success message at the bottom of the output.

Seeing installation errors?

Make sure you included the ! before pip. Without it, Colab treats the line as Python code and throws a syntax error.

Help me troubleshoot this installation error

Next, you need a free Guardrails Hub API key to download validators from the Hub.

  • Open a new browser tab and go to guardrailsai.com/hub.
  • Create a free account (or sign in if you already have one).
  • Navigate to the API Keys page and copy your API key.

Why do I need a Guardrails Hub key?

The Hub hosts community-built validators (like PII detection and competitor checking) as installable packages. Your API key authenticates your CLI so it can download these validators into your notebook.

  • Configure the Guardrails CLI by running this in a new Colab cell:
!guardrails configure

What will this command ask me?

The configuration process asks three interactive questions:

  • Whether you want to enable metrics reporting (enter Yes).
  • Whether you want to use hosted remote inference endpoints for validators (enter Yes).
  • Your API key from the previous step (paste it when prompted).
  • Confirm you see a success message after entering your API key.

Configuration failing?

If you see an authentication error, double-check that you copied the full API key from the Hub page. You can also try the headless version by running the following command with your actual key:

!guardrails configure --token YOUR_TOKEN_HERE

Help me fix this configuration error

Now install the four validators you will use throughout this project.

  • Install the validators from the Hub by running these commands in a new cell:
!guardrails hub install hub://guardrails/detect_pii
!guardrails hub install hub://guardrails/competitor_check
!guardrails hub install hub://guardrails/regex_match
!guardrails hub install hub://guardrails/valid_length

What are these validators?

  • detect_pii scans text for personally identifiable information like emails and phone numbers.
  • competitor_check detects and filters mentions of competitor names from LLM output.
  • regex_match validates text against regular expression patterns.
  • valid_length enforces minimum and maximum character length on responses.
  • Wait for all four installations to complete. Each one prints a confirmation when finished.

Hub install hanging or failing?

Some validators install additional dependencies (like nltk for competitor_check). Give each command up to a minute. If one fails, rerun that single install command in a fresh cell.

Help me fix this hub install error

Get your free Google Gemini API key

Your chatbot needs an LLM to generate responses. Google AI Studio provides free access to Gemini models with no billing required.

  • Open a new browser tab and go to aistudio.google.com.
  • Sign in with your Google account.
  • Accept the Terms of Service if prompted. Google AI Studio automatically creates a default project and API key for new users.
  • Click Get API key in the left sidebar, then click Create API key.
  • Copy your API key and save it somewhere safe. You will paste it into your code in the next substep.

Is the Gemini API really free?

Yes. The Gemini API Free Tier provides free input and output tokens with no billing setup required. This project stays well within free tier limits.

Cannot create an API key?

If you see a permission error, try signing out and back into Google AI Studio. If the issue persists, you can create the key directly in the Google Cloud Console under APIs & Services and then Credentials.

Help me create a Gemini API key

Verify the setup works

With everything installed and configured, you will now write your first code cell that imports Guardrails, sets your Gemini key, and runs a basic test.

  • Create a new Colab cell and paste the following setup code:
import os
from guardrails import Guard
from guardrails.hub import DetectPII, CompetitorCheck, ValidLength, RegexMatch
from guardrails.validators import Validator, PassResult, FailResult, register_validator

# ============================================================
# SETUP
# ============================================================

os.environ['GEMINI_API_KEY'] = "your-gemini-api-key-here"

GEMINI_MODEL = "gemini/gemini-pro"

What does this code do?

  • os.environ['GEMINI_API_KEY'] sets your API key as an environment variable so Guardrails can find it when calling the Gemini API.
  • GEMINI_MODEL stores the model identifier "gemini/gemini-pro" as a constant you will reuse in every Guard call.
  • The imports bring in the Guard class (the core wrapper) and all four Hub validators you installed.
  • The Validator, PassResult, FailResult, and register_validator imports are for the custom validator you will write in a later step.
  • Replace "your-gemini-api-key-here" with the actual Gemini API key you copied from Google AI Studio.
  • Run the cell. If there are no errors, your imports and key are set up correctly.

Now confirm that Guardrails can talk to Gemini by running a quick test.

  • Create a new Colab cell and run this verification code:
# Quick test: call Gemini through a basic Guard with no validators
guard = Guard()
result = guard(
    model=GEMINI_MODEL,
    messages=[{"role": "user", "content": "What is AI?"}]
)
print(result.validated_output)

What should I see?

You should see a text response from Gemini explaining what AI is. The result.validated_output field contains the LLM response after it passes through any validators. In this case none are attached yet, so it passes straight through.

  • Confirm you see a text response printed below the cell. This proves Guardrails and Gemini are connected.

Getting an API error or empty output?

Double-check that you replaced "your-gemini-api-key-here" with your actual key in the setup cell above. Make sure you ran the setup cell before this verification cell.

If you see a quota error, try switching the model to "gemini/gemini-2.5-flash" in the GEMINI_MODEL line and rerun both cells.

Help me debug this Gemini API error

✔️ Awesome, I've got everything!

Great. Make sure you have saved your setup cell. You will build on it in every future step.

ⓧ I'd like to double check the full code

Here is the complete setup cell you should have at this point:

import os
from guardrails import Guard
from guardrails.hub import DetectPII, CompetitorCheck, ValidLength, RegexMatch
from guardrails.validators import Validator, PassResult, FailResult, register_validator

# ============================================================
# SETUP
# ============================================================

os.environ['GEMINI_API_KEY'] = "your-gemini-api-key-here"

GEMINI_MODEL = "gemini/gemini-pro"

Your environment is wired up and ready. Next up, you will build your first input guardrail that blocks personally identifiable information from ever reaching the LLM.

Build Input Guardrails

LLMs process whatever text you send them. If a user accidentally includes their email address or phone number in a prompt, that PII (personally identifiable information) travels to the model's servers and could appear in logs or training data.

In this step, you'll build an input guardrail that intercepts messages before they reach Gemini. The guard scans for PII and either blocks the request entirely or anonymizes the sensitive data before forwarding it.

In this step, get ready to:
  • Create an input guard that blocks PII using the exception strategy.
  • Test the guard with clean and adversarial prompts.
  • Explore the fix strategy that anonymizes PII instead of blocking.
Create a PII detection input guard

Input guardrails validate messages before they reach the LLM. You apply a validator with on="messages" so it runs on the user's input, not the model's output.

  • In your Colab notebook, create a new code cell below your setup cell and add the following code to define your first input guard:
# ============================================================
# STEP 2: INPUT GUARDRAILS - PII DETECTION
# ============================================================

# Input guard with exception behavior
input_guard_exception = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="exception"),
    on="messages"
)

What does this code do?

Guard().use() creates a new guard and attaches a validator to it. The DetectPII validator uses Microsoft Presidio under the hood to scan text for specific PII entity types.

pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"] tells the validator exactly which PII types to look for. The on="messages" parameter means this guard runs on the input messages, not the LLM output. Setting on_fail="exception" means the guard raises an error and blocks the request entirely if PII is detected.

  • Run the cell.
  • You should see the cell complete with no errors. The guard is now defined and ready to use.

Seeing an import error?

Make sure your setup cell (with the imports) ran successfully before this cell. Colab cells run independently, so the imports from the first cell must execute first.

help me debug an import error with DetectPII in my Guardrails AI notebook

Test the input guard with adversarial prompts

A guard is only useful if you can prove it works. You'll send both a clean message and PII-containing messages through the guard to verify it correctly passes safe inputs and blocks dangerous ones.

  • Create a new code cell and add this test function that sends both clean and PII-containing messages through the guard:
# Test input guard - exception mode
def test_input_guard_exception():
    print("--- Input Guard (Exception Mode) ---")

    # Clean message passes
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "What products do you offer?"}]
        )
        print(f"Clean input PASSED: {result.validated_output[:100]}...")
    except Exception as e:
        print(f"Clean input FAILED (unexpected): {e}")

    # PII message blocked
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "Contact me at john@example.com or 555-123-4567"}]
        )
        print(f"PII input PASSED (unexpected): {result.validated_output}")
    except Exception as e:
        print(f"PII input BLOCKED: {e}")

What does this code do?

The function calls the guard the same way you'd call an LLM. You pass model and messages just like a normal API call. The guard intercepts the messages, validates them, and only forwards them to Gemini if they pass.

The try/except blocks handle the ValidationError that gets raised when PII is detected. The clean message should pass through and return a Gemini response. The PII message should raise an exception and never reach the LLM.

  • Run the cell to define the function.
  • Create another new cell and call the function by running:
test_input_guard_exception()
  • You should see the clean input pass with a Gemini response, and the PII input get blocked with an error message mentioning that the text contains PII.

What should I see?

The clean message prints a truncated Gemini response. The PII message prints a blocked message containing the validation error. The error message from Guardrails AI follows the format: "Validation failed for field with errors: The following text in your response contains PII."

Getting a quota or API error?

If you see an error related to quota or the Gemini API, double-check that your GEMINI_API_KEY environment variable is set correctly in your setup cell. Also confirm you accepted the Terms of Service in Google AI Studio.

help me fix a Gemini API quota or authentication error in my Guardrails AI notebook

Explore the fix on_fail action

Blocking every request that contains PII can be too aggressive. Sometimes you want the message to still reach the LLM, but with sensitive data replaced by placeholders. The "fix" strategy does exactly this. It anonymizes the PII and forwards the cleaned message.

  • Create a new code cell and add this second guard that uses the fix strategy instead of exception:
# Input guard with fix behavior (anonymizes PII)
input_guard_fix = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="fix"),
    on="messages"
)

What changed here?

The only difference from the first guard is on_fail="fix" instead of on_fail="exception". Instead of raising an error and halting the request, the guard replaces detected PII with anonymized placeholders (like <EMAIL_ADDRESS>) and then sends the cleaned text to the LLM.

  • Run the cell to define the fix guard.
  • You won't see output yet. The next chunk tests this guard.
  • Create a new code cell and add this test function that sends a PII-containing message through the fix guard:
# Test input guard - fix mode
def test_input_guard_fix():
    print("\n--- Input Guard (Fix Mode) ---")
    result = input_guard_fix(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "My email is john@example.com, can you help?"}]
    )
    print(f"PII input FIXED and sent to LLM: {result.validated_output[:100]}...")

What does this code do?

This function sends a message with an email address through the fix guard. Instead of raising an exception, the guard anonymizes the email before forwarding the message to Gemini. The result.validated_output contains Gemini's response to the cleaned message.

  • Run the cell to define the function, then create a new cell and call it:
test_input_guard_fix()
  • You should see a Gemini response printed. The message reached the LLM because the PII was anonymized rather than blocked.

Exception vs fix: when to use each

Use on_fail="exception" when PII should never reach the LLM under any circumstances. This is the safest approach for high-compliance environments.

Use on_fail="fix" when you want the request to still be processed, but with sensitive data scrubbed. This preserves the user experience while protecting privacy.

Not seeing the fix behavior?

Make sure you're calling test_input_guard_fix() (which uses input_guard_fix), not test_input_guard_exception(). If the cell raises an exception instead of returning a response, you're likely calling the wrong guard.

help me understand why my fix guard is still raising an exception instead of anonymizing PII

✔️ Awesome, I've got everything!

Great. Double check you've run all cells and saved your notebook.

ⓧ I'd like to double check the full code

Here is the complete code for your notebook up to this point:

import os
from guardrails import Guard
from guardrails.hub import DetectPII, CompetitorCheck, ValidLength, RegexMatch
from guardrails.validators import Validator, PassResult, FailResult, register_validator

# ============================================================
# SETUP
# ============================================================

os.environ['GEMINI_API_KEY'] = "your-gemini-api-key-here"

GEMINI_MODEL = "gemini/gemini-pro"

# ============================================================
# STEP 2: INPUT GUARDRAILS - PII DETECTION
# ============================================================

# Input guard with exception behavior
input_guard_exception = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="exception"),
    on="messages"
)

# Input guard with fix behavior (anonymizes PII)
input_guard_fix = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="fix"),
    on="messages"
)

# Test input guard - exception mode
def test_input_guard_exception():
    print("--- Input Guard (Exception Mode) ---")

    # Clean message passes
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "What products do you offer?"}]
        )
        print(f"Clean input PASSED: {result.validated_output[:100]}...")
    except Exception as e:
        print(f"Clean input FAILED (unexpected): {e}")

    # PII message blocked
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "Contact me at john@example.com or 555-123-4567"}]
        )
        print(f"PII input PASSED (unexpected): {result.validated_output}")
    except Exception as e:
        print(f"PII input BLOCKED: {e}")

# Test input guard - fix mode
def test_input_guard_fix():
    print("\n--- Input Guard (Fix Mode) ---")
    result = input_guard_fix(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "My email is john@example.com, can you help?"}]
    )
    print(f"PII input FIXED and sent to LLM: {result.validated_output[:100]}...")

Your input guardrails are working. You've seen how exception mode blocks PII completely, and how fix mode anonymizes it before forwarding to the LLM. Next up, you'll build output guardrails that filter and constrain what Gemini sends back.

Build Output Guardrails

In the previous step, you built input guardrails that stop sensitive data before it ever reaches the LLM.

Now it's time to control what comes back. Even with clean inputs, an LLM can still generate responses that mention competitors, produce wildly long answers, or include content you don't want your users to see. Output guardrails inspect the LLM's response and automatically fix or reject it before it reaches the user.

In this step, get ready to:
  • Create an output guard that filters competitor names from LLM responses.
  • Add response length enforcement to constrain output size.
  • Test the output guard with adversarial prompts that trigger competitor mentions.
Create a competitor check output guard

Your chatbot represents a fictional tech company called TechCorp. If a user asks how TechCorp compares to Apple or Samsung, you don't want the LLM to name those competitors in its answer.

The CompetitorCheck validator scans LLM output for sentences that mention competitors. With on_fail="fix", it removes those sentences entirely rather than blocking the whole response.

  • In your Colab notebook, create a new code cell below your existing input guard code.
  • Add a section comment and define the output guard by pasting the following code:
# ============================================================
# STEP 3: OUTPUT GUARDRAILS - COMPETITOR CHECK + LENGTH
# ============================================================

output_guard = Guard().use(
    CompetitorCheck(competitors=["Apple", "Samsung", "Microsoft"], on_fail="fix"),
).use(
    ValidLength(min=20, max=500, on_fail="fix"),
)

What does this code do?

This creates an output guard with two validators chained together using .use().

  • CompetitorCheck takes a list of competitor names to watch for. When the LLM mentions "Apple", "Samsung", or "Microsoft" in a sentence, that sentence is stripped from the response.
  • ValidLength enforces that responses are between 20 and 500 characters. This prevents one-word answers and walls of text.
  • Both use on_fail="fix" so they repair the output rather than blocking the entire response.

Why fix instead of exception for output guards?

Input guards use on_fail="exception" because you want to stop dangerous data from reaching the LLM entirely. Output guards are different. The LLM already generated a useful response. You just want to clean up specific problems while keeping the rest.

With on_fail="fix", the validator programmatically repairs the output. For CompetitorCheck, this means removing the offending sentences. For ValidLength, this means trimming or padding the response.

Test the output guard with adversarial prompts

Now you need a test function that deliberately asks the LLM to mention competitors so you can see the guard in action.

  • In the same cell (or a new cell below), add the test function by pasting this code:
def test_output_guard():
    print("\n--- Output Guard (Competitor + Length) ---")

    # Ask a question that would normally mention competitors
    result = output_guard(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "Compare your phone to the iPhone and Galaxy S24."}]
    )
    print(f"Competitor-filtered output: {result.validated_output[:200]}...")
    print(f"Validation passed: {result.validation_passed}")

What does this code do?

  • The prompt "Compare your phone to the iPhone and Galaxy S24" is designed to make the LLM mention Apple and Samsung products by name.
  • output_guard(model=GEMINI_MODEL, messages=[...]) sends the prompt to Gemini and validates the response through both validators.
  • result.validated_output contains the cleaned response with competitor sentences removed.
  • result.validation_passed shows whether all validators passed without needing fixes.
  • Run the test function by adding this call at the bottom of the cell and executing it:
test_output_guard()

You should see a response where any sentences mentioning Apple, Samsung, or Microsoft have been stripped. The validation_passed field will likely show False because the fix was applied (the original output failed validation).

Seeing an empty or very short response?

If the LLM's entire response consisted of competitor comparisons, removing those sentences may leave very little text. This is expected behavior. The guard successfully stripped all competitor mentions.

If you see an error about the Gemini API, double check that your GEMINI_API_KEY environment variable is still set correctly in the earlier cell.

Need help? Why is my output guard returning an error or empty response?

Combine input and output guards into a single chatbot flow

You now have two layers of protection. The input guard blocks PII before it reaches the LLM. The output guard filters competitor names and enforces length after the LLM responds.

Let's test both layers back-to-back to see how they work as a pipeline.

  • In a new cell, run both test functions together to see the full pipeline:
# Run both guard tests to see the full pipeline
test_input_guard_exception()
test_output_guard()

You should see the input guard blocking PII messages, followed by the output guard filtering competitor mentions from the LLM's response.

How do input and output guards work together?

In a production chatbot, these guards run in sequence. First, the input guard validates the user's message. If it passes, the message goes to the LLM. Then the output guard validates the LLM's response before showing it to the user.

In the next step, you'll formalize this into a single guarded_chat() function that chains both guards together automatically.

✔️ Awesome, I've got everything!

Great. Double check you've saved all your cells in Colab.

ⓧ I'd like to double check the full code

Here is the complete code you should have so far across your notebook cells:

import os
from guardrails import Guard
from guardrails.hub import DetectPII, CompetitorCheck, ValidLength, RegexMatch
from guardrails.validators import Validator, PassResult, FailResult, register_validator

# ============================================================
# SETUP
# ============================================================

os.environ['GEMINI_API_KEY'] = "your-gemini-api-key-here"

GEMINI_MODEL = "gemini/gemini-pro"

# ============================================================
# STEP 2: INPUT GUARDRAILS - PII DETECTION
# ============================================================

# Input guard with exception behavior
input_guard_exception = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="exception"),
    on="messages"
)

# Input guard with fix behavior (anonymizes PII)
input_guard_fix = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="fix"),
    on="messages"
)

# Test input guard - exception mode
def test_input_guard_exception():
    print("--- Input Guard (Exception Mode) ---")

    # Clean message passes
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "What products do you offer?"}]
        )
        print(f"Clean input PASSED: {result.validated_output[:100]}...")
    except Exception as e:
        print(f"Clean input FAILED (unexpected): {e}")

    # PII message blocked
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "Contact me at john@example.com or 555-123-4567"}]
        )
        print(f"PII input PASSED (unexpected): {result.validated_output}")
    except Exception as e:
        print(f"PII input BLOCKED: {e}")

# Test input guard - fix mode
def test_input_guard_fix():
    print("\n--- Input Guard (Fix Mode) ---")
    result = input_guard_fix(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "My email is john@example.com, can you help?"}]
    )
    print(f"PII input FIXED and sent to LLM: {result.validated_output[:100]}...")

# ============================================================
# STEP 3: OUTPUT GUARDRAILS - COMPETITOR CHECK + LENGTH
# ============================================================

output_guard = Guard().use(
    CompetitorCheck(competitors=["Apple", "Samsung", "Microsoft"], on_fail="fix"),
).use(
    ValidLength(min=20, max=500, on_fail="fix"),
)

def test_output_guard():
    print("\n--- Output Guard (Competitor + Length) ---")

    # Ask a question that would normally mention competitors
    result = output_guard(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "Compare your phone to the iPhone and Galaxy S24."}]
    )
    print(f"Competitor-filtered output: {result.validated_output[:200]}...")
    print(f"Validation passed: {result.validation_passed}")

Your chatbot now has two distinct safety layers. Input guardrails protect the LLM from dangerous data going in. Output guardrails protect the user from unwanted content coming out. Next up, you'll write your own custom validator from scratch to enforce topic boundaries.

Write a Custom Validator

Your chatbot now has pre-built guardrails that block PII and filter competitor names. These are powerful, but they only cover generic safety scenarios.

What happens when an LLM starts answering questions about politics or astrology instead of your company's products? Pre-built validators can't enforce your specific business rules. In this step, you'll write a custom validator from scratch that keeps responses on-topic, then wire everything into a single guarded chatbot function.

In this step, get ready to:
  • Create a custom TopicGuard validator class that detects off-topic content.
  • Integrate all guardrails into a single chatbot function.
  • Run an adversarial test suite that demonstrates every guardrail in action.
Create a custom validator class

Pre-built Hub validators handle common patterns like PII or competitor names. When you need to enforce rules unique to your application, you write a custom validator class.

  • In your Colab notebook, add a new code cell below your existing output guard code.
  • Add the section header comment and the TopicGuard class by pasting this code:
# ============================================================
# STEP 4: CUSTOM VALIDATOR - TOPIC BOUNDARY ENFORCEMENT
# ============================================================

@register_validator(name="custom/topic_guard", data_type="string")
class TopicGuard(Validator):
    """Validates that LLM output stays within allowed topics."""

    def __init__(self, blocked_keywords=None, on_fail="exception", **kwargs):
        super().__init__(on_fail=on_fail, blocked_keywords=blocked_keywords, **kwargs)
        self.blocked_keywords = blocked_keywords or [
            "election", "candidate", "democrat", "republican",
            "prayer", "scripture", "worship",
            "horoscope", "zodiac", "astrology"
        ]

    def validate(self, value, metadata=None):
        """Check if the response contains off-topic content."""
        value_lower = value.lower()
        triggered_keywords = [
            kw for kw in self.blocked_keywords if kw in value_lower
        ]

        if triggered_keywords:
            return FailResult(
                error_message=f"Response contains off-topic content: {triggered_keywords}",
            )
        return PassResult()

What does this code do?

The @register_validator decorator registers your class with Guardrails AI so it can be used in Guard chains just like Hub validators.

  • name="custom/topic_guard" gives it a unique identifier. The custom/ prefix is a convention to distinguish your validators from Hub ones.
  • data_type="string" tells Guardrails this validator works on string outputs.
  • The validate method receives the LLM's output as value, lowercases it, and checks for any blocked keywords.
  • If a keyword is found, FailResult triggers the on_fail action. If all clear, PassResult lets the response through.

Why blocked keywords instead of allowed topics?

A blocklist approach is more practical for LLM outputs. Allowed-topic enforcement would require understanding the semantic meaning of every response. Keyword blocking catches the most common off-topic patterns with minimal false positives.

The system prompt (coming in the next chunk) handles allowed-topic guidance by instructing the LLM what to discuss. The validator acts as a safety net for when the LLM ignores its instructions.

  • Run the cell to register the validator.

You won't see any output yet. The class is registered and ready to use in the next chunk.

Seeing a NameError?

Make sure your first cell (with the imports) has already been run. The Validator, PassResult, FailResult, and register_validator are all imported from guardrails.validators in your setup cell.

help me fix a NameError in my custom validator cell

Integrate the custom validator into the chatbot

Now you need to combine all your guardrails into a single output guard and build a chat function that routes every message through the full pipeline.

  • Add a new code cell and create the combined output guard by pasting this code:
# Combined output guard with all validators
full_output_guard = Guard().use(
    CompetitorCheck(competitors=["Apple", "Samsung", "Microsoft"], on_fail="fix"),
).use(
    ValidLength(min=20, max=500, on_fail="fix"),
).use(
    TopicGuard(on_fail="exception"),
)

What does this code do?

This chains three validators into one Guard. Each .use() call adds another validation layer that runs sequentially on the LLM's output.

  • CompetitorCheck uses on_fail="fix" to silently remove competitor names from responses.
  • ValidLength uses on_fail="fix" to trim responses that exceed 500 characters.
  • TopicGuard uses on_fail="exception" to completely block off-topic responses. Off-topic content can't be "fixed" so blocking is the right strategy.
  • Run the cell to create the guard.

You won't see output yet. The next chunk builds the chat function that uses this guard.

  • Add another new code cell and create the guarded_chat function by pasting this code:
# ============================================================
# COMPLETE CHATBOT FUNCTION
# ============================================================

def guarded_chat(user_message):
    """Send a message through the full guardrail pipeline."""
    # Step 1: Validate input (PII check)
    try:
        input_guard_exception.validate(user_message)
    except Exception as e:
        return f"INPUT BLOCKED: {e}"

    # Step 2: Call LLM with output guardrails
    try:
        result = full_output_guard(
            model=GEMINI_MODEL,
            messages=[
                {"role": "system", "content": "You are a helpful assistant for TechCorp. Only discuss TechCorp products, pricing, and support."},
                {"role": "user", "content": user_message}
            ]
        )
        return result.validated_output
    except Exception as e:
        return f"OUTPUT BLOCKED: {e}"

What does this code do?

This function is the single entry point for your guarded chatbot. Every user message passes through two layers of protection:

  • First, input_guard_exception.validate() checks the raw user message for PII before it ever reaches the LLM.
  • Then, full_output_guard calls Gemini with a system prompt restricting the assistant to TechCorp topics, and validates the response through all three output validators.
  • If either layer fails, the function returns a clear INPUT BLOCKED or OUTPUT BLOCKED message instead of the LLM response.
  • Run the cell to define the function.
  • Test it by adding a new cell with this quick check:
# Quick test - should pass through to Gemini
print(guarded_chat("What products do you sell?"))
  • Run the cell. You should see a response from Gemini about TechCorp's products.

Seeing INPUT BLOCKED or OUTPUT BLOCKED on a clean message?

If a normal question gets blocked, check that your input_guard_exception cell ran successfully earlier. Also confirm GEMINI_API_KEY is still set by running os.environ['GEMINI_API_KEY'] in a new cell.

help me debug my guarded_chat function returning unexpected blocked messages

  • Now test the topic guard by running this in a new cell:
# Test off-topic blocking
print(guarded_chat("What are your thoughts on the upcoming election?"))
  • You should see OUTPUT BLOCKED followed by an error message mentioning off-topic content. Your custom validator is working.
Test the complete guarded chatbot

Individual tests prove each guardrail works in isolation. A proper test suite runs adversarial prompts against the full pipeline and shows you which guardrails triggered at a glance.

  • Add a new code cell and create the test suite by pasting this code:
# ============================================================
# TEST SUITE
# ============================================================

def run_test_suite():
    """Run adversarial prompts against all guardrails."""
    test_cases = [
        ("What products do you sell?", "Normal question"),
        ("Email me at test@email.com with details", "PII in input"),
        ("How does your product compare to the iPhone?", "Competitor mention"),
        ("What are your thoughts on the upcoming election?", "Off-topic (politics)"),
        ("Tell me about your pricing plans", "Normal question"),
    ]

    print("\n" + "=" * 60)
    print("FULL GUARDRAIL TEST SUITE")
    print("=" * 60)

    results = []
    for prompt, description in test_cases:
        response = guarded_chat(prompt)
        blocked = response.startswith("INPUT BLOCKED") or response.startswith("OUTPUT BLOCKED")
        status = "BLOCKED" if blocked else "PASSED"
        results.append((description, status, response[:80]))
        print(f"\n[{status}] {description}")
        print(f"  Prompt: {prompt}")
        print(f"  Response: {response[:80]}...")

    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"{'Test Case':<30} {'Status':<10}")
    print("-" * 40)
    for desc, status, _ in results:
        print(f"{desc:<30} {status:<10}")

# Run all tests
if __name__ == "__main__":
    test_input_guard_exception()
    test_input_guard_fix()
    test_output_guard()
    run_test_suite()

What does this code do?

The test suite sends five carefully chosen prompts that each target a different guardrail:

  • Two normal questions that should pass through cleanly.
  • One PII prompt (email address) to trigger the input guard.
  • One competitor prompt (iPhone) to trigger CompetitorCheck.
  • One off-topic prompt (election) to trigger your custom TopicGuard.

The summary table at the end gives you a quick overview of which prompts were blocked vs which passed through.

  • Run the cell. You should see each test case print with its status, followed by a summary table.

In the summary table, you should see "Normal question" marked PASSED, "PII in input" marked BLOCKED, "Competitor mention" marked PASSED (because on_fail="fix" silently repairs instead of blocking), and "Off-topic (politics)" marked BLOCKED.

Test results don't match expectations?

If the PII test shows PASSED instead of BLOCKED, the input_guard_exception cell may not have run. Re-run your cells from the top in order.

If the competitor test shows BLOCKED instead of PASSED, remember that on_fail="fix" silently removes competitor names without raising an exception. The response still comes through, just cleaned.

help me understand why my test suite results don't match the expected output

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook.

ⓧ I'd like to double check the full code

Here is the complete guardrails_chatbot.py file up to this point:

import os
from guardrails import Guard
from guardrails.hub import DetectPII, CompetitorCheck, ValidLength, RegexMatch
from guardrails.validators import Validator, PassResult, FailResult, register_validator

# ============================================================
# SETUP
# ============================================================

os.environ['GEMINI_API_KEY'] = "your-gemini-api-key-here"

GEMINI_MODEL = "gemini/gemini-pro"

# ============================================================
# STEP 2: INPUT GUARDRAILS - PII DETECTION
# ============================================================

# Input guard with exception behavior
input_guard_exception = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="exception"),
    on="messages"
)

# Input guard with fix behavior (anonymizes PII)
input_guard_fix = Guard().use(
    DetectPII(pii_entities=["EMAIL_ADDRESS", "PHONE_NUMBER"], on_fail="fix"),
    on="messages"
)

# Test input guard - exception mode
def test_input_guard_exception():
    print("--- Input Guard (Exception Mode) ---")

    # Clean message passes
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "What products do you offer?"}]
        )
        print(f"Clean input PASSED: {result.validated_output[:100]}...")
    except Exception as e:
        print(f"Clean input FAILED (unexpected): {e}")

    # PII message blocked
    try:
        result = input_guard_exception(
            model=GEMINI_MODEL,
            messages=[{"role": "user", "content": "Contact me at john@example.com or 555-123-4567"}]
        )
        print(f"PII input PASSED (unexpected): {result.validated_output}")
    except Exception as e:
        print(f"PII input BLOCKED: {e}")

# Test input guard - fix mode
def test_input_guard_fix():
    print("\n--- Input Guard (Fix Mode) ---")
    result = input_guard_fix(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "My email is john@example.com, can you help?"}]
    )
    print(f"PII input FIXED and sent to LLM: {result.validated_output[:100]}...")

# ============================================================
# STEP 3: OUTPUT GUARDRAILS - COMPETITOR CHECK + LENGTH
# ============================================================

output_guard = Guard().use(
    CompetitorCheck(competitors=["Apple", "Samsung", "Microsoft"], on_fail="fix"),
).use(
    ValidLength(min=20, max=500, on_fail="fix"),
)

def test_output_guard():
    print("\n--- Output Guard (Competitor + Length) ---")

    # Ask a question that would normally mention competitors
    result = output_guard(
        model=GEMINI_MODEL,
        messages=[{"role": "user", "content": "Compare your phone to the iPhone and Galaxy S24."}]
    )
    print(f"Competitor-filtered output: {result.validated_output[:200]}...")
    print(f"Validation passed: {result.validation_passed}")

# ============================================================
# STEP 4: CUSTOM VALIDATOR - TOPIC BOUNDARY ENFORCEMENT
# ============================================================

@register_validator(name="custom/topic_guard", data_type="string")
class TopicGuard(Validator):
    """Validates that LLM output stays within allowed topics."""

    def __init__(self, blocked_keywords=None, on_fail="exception", **kwargs):
        super().__init__(on_fail=on_fail, blocked_keywords=blocked_keywords, **kwargs)
        self.blocked_keywords = blocked_keywords or [
            "election", "candidate", "democrat", "republican",
            "prayer", "scripture", "worship",
            "horoscope", "zodiac", "astrology"
        ]

    def validate(self, value, metadata=None):
        """Check if the response contains off-topic content."""
        value_lower = value.lower()
        triggered_keywords = [
            kw for kw in self.blocked_keywords if kw in value_lower
        ]

        if triggered_keywords:
            return FailResult(
                error_message=f"Response contains off-topic content: {triggered_keywords}",
            )
        return PassResult()

# Combined output guard with all validators
full_output_guard = Guard().use(
    CompetitorCheck(competitors=["Apple", "Samsung", "Microsoft"], on_fail="fix"),
).use(
    ValidLength(min=20, max=500, on_fail="fix"),
).use(
    TopicGuard(on_fail="exception"),
)

# ============================================================
# COMPLETE CHATBOT FUNCTION
# ============================================================

def guarded_chat(user_message):
    """Send a message through the full guardrail pipeline."""
    # Step 1: Validate input (PII check)
    try:
        input_guard_exception.validate(user_message)
    except Exception as e:
        return f"INPUT BLOCKED: {e}"

    # Step 2: Call LLM with output guardrails
    try:
        result = full_output_guard(
            model=GEMINI_MODEL,
            messages=[
                {"role": "system", "content": "You are a helpful assistant for TechCorp. Only discuss TechCorp products, pricing, and support."},
                {"role": "user", "content": user_message}
            ]
        )
        return result.validated_output
    except Exception as e:
        return f"OUTPUT BLOCKED: {e}"

# ============================================================
# TEST SUITE
# ============================================================

def run_test_suite():
    """Run adversarial prompts against all guardrails."""
    test_cases = [
        ("What products do you sell?", "Normal question"),
        ("Email me at test@email.com with details", "PII in input"),
        ("How does your product compare to the iPhone?", "Competitor mention"),
        ("What are your thoughts on the upcoming election?", "Off-topic (politics)"),
        ("Tell me about your pricing plans", "Normal question"),
    ]

    print("\n" + "=" * 60)
    print("FULL GUARDRAIL TEST SUITE")
    print("=" * 60)

    results = []
    for prompt, description in test_cases:
        response = guarded_chat(prompt)
        blocked = response.startswith("INPUT BLOCKED") or response.startswith("OUTPUT BLOCKED")
        status = "BLOCKED" if blocked else "PASSED"
        results.append((description, status, response[:80]))
        print(f"\n[{status}] {description}")
        print(f"  Prompt: {prompt}")
        print(f"  Response: {response[:80]}...")

    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    print(f"{'Test Case':<30} {'Status':<10}")
    print("-" * 40)
    for desc, status, _ in results:
        print(f"{desc:<30} {status:<10}")

# Run all tests
if __name__ == "__main__":
    test_input_guard_exception()
    test_input_guard_fix()
    test_output_guard()
    run_test_suite()

Your chatbot is now fully guarded. Every message passes through PII detection on input, then competitor filtering, length enforcement, and topic boundary checking on output. Next up, you'll take on a Secret Mission to add LLM-powered toxic language detection.

Secret mission

Add LLM-Based Toxic Language Detection

Your guardrails catch PII, competitor mentions, and off-topic content. But what about toxic language? In this bonus challenge, you'll add an LLM-powered validator that uses Gemini itself to detect and strip toxic sentences from chatbot responses.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely in Google Colab using free-tier APIs, so there are no ongoing costs. Decide whether to keep your resources available, pause them to come back later, or delete them entirely.

Resources you used:

  • Google Colab notebook (stored in Google Drive).
  • Gemini API key (managed in Google Cloud Console).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your guarded chatbot or want to revisit the notebook later.

  • Your Colab notebook stays in Google Drive and you can reopen it anytime.
  • Your Gemini API key remains active on the free tier with no charges.
  • Your Guardrails Hub account and API key also remain active at no cost.

Pause - I'll come back to this later

Free up Colab resources while keeping your files intact. You can resume where you left off next time.

  • In your Colab notebook, click Runtime in the top menu, then click Disconnect and delete runtime.
  • This shuts down the virtual machine and frees memory, but your notebook file stays in Google Drive.
  • Next time you open the notebook, you will need to re-run the install cells to restore your packages.

Delete - I don't want to use this again

Remove all project resources and revoke API access. Follow each step below.

Delete your Colab notebook
  • Go to drive.google.com.
  • Find your guardrails chatbot notebook, right-click it, and click Move to Trash.
  • On the left sidebar, click Trash, select the notebook, and click Delete forever to permanently remove it.
Delete your Gemini API key
  • Open the Google Cloud API Credentials page at console.cloud.google.com/apis/credentials.
  • Find the API key you created for this project and click the Actions icon (three dots).
  • Select Delete API key and confirm the deletion in the dialog.

Nice Work!

Nice Work!

Nice work! You just built a production-ready guarded AI chatbot that wraps Google Gemini with layered safety validators using Guardrails AI.

You've learned how to:

  • Built input guardrails that detect and block PII before it ever reaches the LLM, using both exception and fix strategies.
  • Implemented output guardrails that filter competitor mentions and enforce response length constraints on LLM responses.
  • Wrote a custom validator from scratch using the @register_validator decorator to enforce topic boundaries on chatbot output.
  • Secret Mission: Added LLM-based toxic language detection that uses Gemini itself to evaluate and strip harmful content at the sentence level.

Ready to quiz yourself?