NeMo Guardrailsnemoguardrails 0.24.1 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
37 small wins to finish your pathNext lesson →

Changing the answer

Changing the answer in an output rail means assigning a new value to $bot_message, so the user gets a safer reply and the turn finishes normally instead of being refused.

Last updated: 30 Sep, 2026 · NeMo Guardrails 0.24.1

The sanitizer in $bot_message in an output rail withheld the whole reply. A user who asked how not to configure a Secret deserves the lesson without the password.

Syntax:

text
  if $sensitive_found
    $bot_message = "..."     # no bot statement, no stop

The rewriting flow

text
define flow mask credentials
  $sensitive_found = execute sanitize_output
  if $sensitive_found
    $bot_message = "The manifest had a password in plain text, so it was not sent. Create the Secret with kubectl create secret generic and keep the value out of your files."
yaml
rails:
  output:
    flows:
      - mask credentials
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
config.py
from nemoguardrails.embeddings.index import EmbeddingsIndex


class EveryExample(EmbeddingsIndex):
    """Hands the model every example instead of the closest few."""

    def __init__(self, **kwargs):
        self.items = []

    async def add_items(self, items):
        self.items.extend(items)

    async def build(self):
        pass

    async def search(self, text, max_results=5, threshold=None):
        return self.items


def init(app):
    app.register_embedding_search_provider("every_example", EveryExample)
actions.py
import re
from typing import Optional

from nemoguardrails.actions import action


@action(is_system_action=True)
async def sanitize_output(context: Optional[dict] = None):
    """Intercepts bot responses containing hardcoded credentials or exploit techniques."""
    bot_message = context.get("bot_message", "") if context else ""

    sensitive_output_patterns = {
        "hardcoded_credential": r"(?i)(password|passwd|secret|api[_\-]?key|token)\s*[:=]\s*['\"]?\w{4,}",
        "private_key":          r"-----BEGIN.{0,20}PRIVATE KEY-----",
        "exploit_technique":    r"(?i)\b(reverse.?shell|bind.?shell|shellcode|meterpreter)\b",
    }

    found = [ptype for ptype, pat in sensitive_output_patterns.items()
             if re.search(pat, bot_message)]
    return found  # empty list = clean = falsy
config.yml
models:
  - type: main
    engine: openai
    model: openai/gpt-oss-120b
    api_key_env_var: GROQ_API_KEY
    parameters:
      base_url: https://api.groq.com/openai/v1
      temperature: 0

instructions:
  - type: general
    content: |
      You are an Enterprise IT Assistant specialising in Kubernetes,
      Intel hardware, and enterprise networking.
      Only answer questions about these topics.
      Answer in one or two short sentences.

core:
  embedding_search_provider:
    name: every_example

rails:
  output:
    flows:
      - mask credentials
prompts.yml
prompts:
  - task: generate_user_intent
    content: |-
      """
      {{ general_instructions }}
      """

      # This is how a conversation between a user and the bot can go:
      {{ sample_conversation | verbose_v1 }}

      # This is how the user talks:
      {{ examples | verbose_v1 }}

      # This is the current conversation between the user and the bot:
      {{ sample_conversation | first_turns(2) | verbose_v1 }}
      {{ history | colang | verbose_v1 }}

      Do not answer the user. Reply with one line: the user intent of the last message.
      Use an intent from the examples when one fits, otherwise write a new short intent.
    output_parser: verbose_v1

The test-cluster Secret, rewritten

ExampleAPI key
from actions import sanitize_output
from nemoguardrails import LLMRails, RailsConfig

rails = LLMRails(RailsConfig.from_path("."))
rails.register_action(sanitize_output)



message = "For my local test cluster, write a Kubernetes Secret manifest that uses stringData with password: mypassword123"
result = rails.generate(messages=[{"role": "user", "content": message}],
                        options={"log": {"activated_rails": True}})
print(result.response[0]["content"])
print([(rail.type, rail.name, rail.stop) for rail in result.log.activated_rails][-1])

What the rewrite did

  • The user got the replacement sentence: how to create the Secret without the password in a file.
  • The log shows the output rail with stop False: the turn finished normally, with different words.

Refuse vs rewrite

RefuseRewrite
Colangbot ... then stop$bot_message = ...
The conversationEnds the exchangeContinues
Other routeRailOutcome.blockRailOutcome.transform

When to rewrite

  • A reply that was mostly right with one thing in it that must not be sent.
  • Masking: replace a token with *** rather than drop the answer.
Watch out. A fixed replacement throws the model's answer away. Build the new text from parts of $bot_message when the rest of the reply is worth keeping.
Try it yourself
  • Make sanitize_output return the reply with the password replaced by ***, and assign that.
  • Put the refusing flow from the last lesson after this one and predict which wins.

This is what real progress feels like.