NVIDIA-NeMo / NVIDIA-NeMo/Guardrails

bug: GenerationLog.print_summary() crashes with stats.total_duration or activated_rail.duration None values

Open
#2,206 3 comments 0 reactions 1 assignee View on GitHub

@christinaexyou is already working on this.

Since Aug 24, 2026.

bug
Dominant language
Python
Stars
7.2k
Forks
842
Avg merge
3d 1h
Merged PRs (30d)
25

Description

Did you check docs and existing issues?
  • I have read all the NeMo-Guardrails docs
  • I have updated the package to the latest version before submitting this issue
  • (optional) I have used the develop branch
  • I have searched the existing issues of NeMo-Guardrails
Python version (python --version)

Python 3.13.2

Operating system/version

MacOS 26.5.2

NeMo-Guardrails version (if you must use a specific version and not the latest

0.24

Describe the bug

The stats.total_duration and activated_rail.duration fields are Optional[float] fields.

The GenerationLog.print_summary() doesn't have None-guards for all these fields, which will throw errors when it tries to print None with a float format.

Steps To Reproduce

Create

Expected Behavior

GenerationLog.print_summary() should correctly guard None values rather than crashing. With the option of OTEL tracing, the value of print_summary() is debatable

Actual Behavior

Create the script below in print_summary.py, and run it using uv run --locked python print_summary.py

"""Reproduce TypeError in GenerationLog.print_summary() on None duration fields.

print_summary() (nemoguardrails/rails/llm/options.py) formats several
Optional[float] duration fields with ``:.2f`` without guarding against None, so
any log carrying a None duration raises:

    TypeError: unsupported format string passed to NoneType.__format__

There are two independent, unguarded crash points:

  1. GenerationStats.total_duration  (options.py ~line 318)
     This field DEFAULTS to None, so a freshly built GenerationLog whose stats
     were never populated crashes on the very first print, before any rail is
     inspected.

  2. ActivatedRail.duration  (options.py ~line 370, the "Detailed stats" loop)
     ActivatedRail.duration is Optional[float] and tool rails come through with
     duration=None, so they crash the per-rail loop even when the stats block is
     fully populated.

Both are reproduced below; each is caught so the script runs to completion and
prints a full traceback pointing at the exact failing line.

Run:  python print_summary.py
"""

import sys
import traceback

import nemoguardrails
from nemoguardrails.rails.llm.options import (
    ActivatedRail,
    ExecutedAction,
    GenerationLog,
    GenerationStats,
)


def crash_1_total_duration() -> None:
    """GenerationStats.total_duration defaults to None and crashes line ~318.

    The activated rail here carries a valid duration; only stats is left at its
    default, isolating total_duration as the field that crashes.
    """
    log = GenerationLog(
        activated_rails=[
            ActivatedRail(type="tool_output", name="tool output check", duration=0.5),
        ],
    )
    log.print_summary()


def crash_2_activated_rail_duration() -> None:
    """A tool rail with duration=None crashes the detailed-stats loop, line ~370.

    stats.total_duration is populated so execution gets past crash point #1 and
    reaches the per-rail loop, isolating ActivatedRail.duration as the field
    that crashes.
    """
    log = GenerationLog(
        activated_rails=[
            ActivatedRail(
                type="tool_output",
                name="tool output check",
                executed_actions=[ExecutedAction(action_name="tool_output_check")],
                duration=None,
            ),
        ],
        stats=GenerationStats(total_duration=0.5),
    )
    log.print_summary()


def run(label: str, fn) -> None:
    print(f"\n{'=' * 72}\n{label}\n{'=' * 72}")
    try:
        fn()
    except TypeError:
        print("REPRODUCED - TypeError raised:\n")
        traceback.print_exc()
    else:
        print("No error raised - bug NOT reproduced.")


if __name__ == "__main__":
    print(f"Python:        {sys.version.split()[0]}")
    print(f"nemoguardrails: {getattr(nemoguardrails, '__version__', 'unknown')}")

    run(
        "Crash 1: GenerationStats.total_duration is None (options.py ~line 318)",
        crash_1_total_duration,
    )
    run(
        "Crash 2: ActivatedRail.duration is None (options.py ~line 370)",
        crash_2_activated_rail_duration,
    )

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.