railtracks.guardrails

 1from . import llm
 2from .core import (
 3    BaseGuardrail,
 4    BaseLLMGuardrail,
 5    Guard,
 6    Guardrail,
 7    GuardrailAction,
 8    GuardrailBlockedError,
 9    GuardrailDecision,
10    GuardrailTrace,
11    InputGuard,
12    LLMGuardrailEvent,
13    LLMGuardrailPhase,
14    OutputGuard,
15)
16
17__all__ = [
18    "Guard",
19    "Guardrail",
20    "BaseGuardrail",
21    "GuardrailAction",
22    "GuardrailBlockedError",
23    "GuardrailDecision",
24    "GuardrailTrace",
25    "BaseLLMGuardrail",
26    "InputGuard",
27    "OutputGuard",
28    "LLMGuardrailEvent",
29    "LLMGuardrailPhase",
30    "llm",
31]
class Guard(pydantic.main.BaseModel):
11class Guard(BaseModel):
12    """
13    Configuration for guardrails: input/output (and future tool) rails plus behavior flags.
14
15    ``input`` and ``output`` are lists of LLM guardrails (see :class:`BaseLLMGuardrail`).
16    The runner expects each rail to be callable with :class:`LLMGuardrailEvent` and
17    return a :class:`GuardrailDecision`. For output rails, the guarded assistant
18    message is ``event.output_message`` (see :class:`~railtracks.guardrails.core.event.LLMGuardrailEvent`).
19
20    Attributes:
21        input: LLM input guardrails (prompt / message history).
22        output: LLM output guardrails (model response).
23        tool_call: Reserved for future tool-call guardrails (not yet wired).
24        tool_response: Reserved for future tool-response guardrails (not yet wired).
25        fail_open: If True, a rail exception, bad transform, or unknown action is
26            recorded but does not stop the chain; if False, the runner stops and
27            returns a blocking decision.
28        trace: Reserved to toggle whether nodes attach per-rail traces (e.g. to
29            ``details``); the mixin currently always collects traces when rails run.
30    """
31
32    model_config = ConfigDict(arbitrary_types_allowed=True)
33
34    input: list[InputGuard] = Field(
35        default_factory=list,
36        description="Guardrails run on LLM input (prompt / message history).",
37    )
38    output: list[OutputGuard] = Field(
39        default_factory=list,
40        description="Guardrails run on LLM output (model response).",
41    )
42    tool_call: list[Any] = Field(default_factory=list)
43    tool_response: list[Any] = Field(default_factory=list)
44    fail_open: bool = False
45    trace: bool = True
46
47    @field_validator("input", "output", "tool_call", "tool_response")
48    @classmethod
49    def _validate_callable_rails(
50        cls, value: list[InputGuard | OutputGuard]
51    ) -> list[InputGuard | OutputGuard]:
52        for rail in value:
53            if not callable(rail):
54                raise TypeError(
55                    f"Every guardrail must be callable, got {type(rail).__name__}."
56                )
57        return value

Configuration for guardrails: input/output (and future tool) rails plus behavior flags.

input and output are lists of LLM guardrails (see BaseLLMGuardrail). The runner expects each rail to be callable with LLMGuardrailEvent and return a GuardrailDecision. For output rails, the guarded assistant message is event.output_message (see ~railtracks.guardrails.core.event.LLMGuardrailEvent).

Attributes:
  • input: LLM input guardrails (prompt / message history).
  • output: LLM output guardrails (model response).
  • tool_call: Reserved for future tool-call guardrails (not yet wired).
  • tool_response: Reserved for future tool-response guardrails (not yet wired).
  • fail_open: If True, a rail exception, bad transform, or unknown action is recorded but does not stop the chain; if False, the runner stops and returns a blocking decision.
  • trace: Reserved to toggle whether nodes attach per-rail traces (e.g. to details); the mixin currently always collects traces when rails run.
model_config = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

input: list[InputGuard]
output: list[OutputGuard]
tool_call: list[typing.Any]
tool_response: list[typing.Any]
fail_open: bool
trace: bool
class Guardrail(typing.Protocol):
14class Guardrail(Protocol):
15    """
16    Base protocol for all guardrails: callable with a name.
17
18    Concrete ABC hierarchies (e.g. :class:`BaseLLMGuardrail`) narrow the event
19    type and add domain-specific attributes like ``phase``.
20    """
21
22    name: str
23
24    def __call__(self, event: Any) -> GuardrailDecision: ...

Base protocol for all guardrails: callable with a name.

Concrete ABC hierarchies (e.g. BaseLLMGuardrail) narrow the event type and add domain-specific attributes like phase.

Guardrail(*args, **kwargs)
1431def _no_init_or_replace_init(self, *args, **kwargs):
1432    cls = type(self)
1433
1434    if cls._is_protocol:
1435        raise TypeError('Protocols cannot be instantiated')
1436
1437    # Already using a custom `__init__`. No need to calculate correct
1438    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1439    if cls.__init__ is not _no_init_or_replace_init:
1440        return
1441
1442    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1443    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1444    # searches for a proper new `__init__` in the MRO. The new `__init__`
1445    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1446    # instantiation of the protocol subclass will thus use the new
1447    # `__init__` and no longer call `_no_init_or_replace_init`.
1448    for base in cls.__mro__:
1449        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1450        if init is not _no_init_or_replace_init:
1451            cls.__init__ = init
1452            break
1453    else:
1454        # should not happen
1455        cls.__init__ = object.__init__
1456
1457    cls.__init__(self, *args, **kwargs)
name: str
class BaseGuardrail(abc.ABC):
27class BaseGuardrail(ABC):
28    """Abstract base class for all guardrails."""
29
30    name: str
31
32    def __init__(self, name: str | None = None):
33        """Initialize the guardrail.
34
35        Args:
36            name: Rail name for traces and debugging; defaults to the class name.
37        """
38        self.name = name or self.__class__.__name__
39
40    @abstractmethod
41    def __call__(self, event: Any) -> GuardrailDecision:
42        pass

Abstract base class for all guardrails.

BaseGuardrail(name: str | None = None)
32    def __init__(self, name: str | None = None):
33        """Initialize the guardrail.
34
35        Args:
36            name: Rail name for traces and debugging; defaults to the class name.
37        """
38        self.name = name or self.__class__.__name__

Initialize the guardrail.

Arguments:
  • name: Rail name for traces and debugging; defaults to the class name.
name: str
class GuardrailAction(builtins.str, enum.Enum):
12class GuardrailAction(str, Enum):
13    """What the runner should do after a guardrail returns.
14
15    Members:
16        ALLOW: Keep the current input or output unchanged.
17        TRANSFORM: Replace input messages or the output message from the decision.
18        BLOCK: Stop and treat the interaction as blocked.
19    """
20
21    ALLOW = "allow"
22    TRANSFORM = "transform"
23    BLOCK = "block"

What the runner should do after a guardrail returns.

Members:

ALLOW: Keep the current input or output unchanged. TRANSFORM: Replace input messages or the output message from the decision. BLOCK: Stop and treat the interaction as blocked.

ALLOW = <GuardrailAction.ALLOW: 'allow'>
TRANSFORM = <GuardrailAction.TRANSFORM: 'transform'>
BLOCK = <GuardrailAction.BLOCK: 'block'>
class GuardrailBlockedError(railtracks.exceptions.errors.NodeInvocationError):
12class GuardrailBlockedError(NodeInvocationError):
13    """
14    Raised when guardrails deterministically block an operation (e.g. LLM input).
15
16    This error is intended to remain distinguishable from `LLMError` so callers/tests can
17    assert guardrail rejection explicitly.
18    """
19
20    def __init__(
21        self,
22        *,
23        rail_name: str | None = None,
24        reason: str,
25        user_facing_message: str | None = None,
26        traces: list["GuardrailTrace"] | None = None,
27        meta: dict[str, Any] | None = None,
28        notes: list[str] | None = None,
29        fatal: bool = False,
30    ):
31        """Create a block error with optional trace and user-facing context.
32
33        Args:
34            rail_name: Name of the rail that blocked, if known.
35            reason: Machine-oriented explanation (also embedded in the base message).
36            user_facing_message: Optional short text for clients or UIs.
37            traces: Optional list of :class:`~railtracks.guardrails.core.trace.GuardrailTrace`
38                from the failed run.
39            meta: Optional structured details copied from the blocking decision.
40            notes: Extra debug lines forwarded to :class:`NodeInvocationError`.
41            fatal: Passed through to :class:`NodeInvocationError` (whether the run is
42                considered unrecoverable).
43        """
44        self.rail_name = rail_name
45        self.reason = reason
46        self.user_facing_message = user_facing_message
47        self.traces = traces
48        self.meta = meta
49
50        base_message = "Blocked by guardrails"
51        if rail_name:
52            base_message += f" ({rail_name})"
53        base_message += f": {reason}"
54
55        derived_notes: list[str] = []
56        if user_facing_message:
57            derived_notes.append(f"user_message={user_facing_message!r}")
58        if meta:
59            derived_notes.append("meta attached (see exception.meta)")
60
61        super().__init__(
62            message=base_message,
63            notes=[*(notes or []), *derived_notes],
64            fatal=fatal,
65        )

Raised when guardrails deterministically block an operation (e.g. LLM input).

This error is intended to remain distinguishable from LLMError so callers/tests can assert guardrail rejection explicitly.

GuardrailBlockedError( *, rail_name: str | None = None, reason: str, user_facing_message: str | None = None, traces: list[GuardrailTrace] | None = None, meta: dict[str, typing.Any] | None = None, notes: list[str] | None = None, fatal: bool = False)
20    def __init__(
21        self,
22        *,
23        rail_name: str | None = None,
24        reason: str,
25        user_facing_message: str | None = None,
26        traces: list["GuardrailTrace"] | None = None,
27        meta: dict[str, Any] | None = None,
28        notes: list[str] | None = None,
29        fatal: bool = False,
30    ):
31        """Create a block error with optional trace and user-facing context.
32
33        Args:
34            rail_name: Name of the rail that blocked, if known.
35            reason: Machine-oriented explanation (also embedded in the base message).
36            user_facing_message: Optional short text for clients or UIs.
37            traces: Optional list of :class:`~railtracks.guardrails.core.trace.GuardrailTrace`
38                from the failed run.
39            meta: Optional structured details copied from the blocking decision.
40            notes: Extra debug lines forwarded to :class:`NodeInvocationError`.
41            fatal: Passed through to :class:`NodeInvocationError` (whether the run is
42                considered unrecoverable).
43        """
44        self.rail_name = rail_name
45        self.reason = reason
46        self.user_facing_message = user_facing_message
47        self.traces = traces
48        self.meta = meta
49
50        base_message = "Blocked by guardrails"
51        if rail_name:
52            base_message += f" ({rail_name})"
53        base_message += f": {reason}"
54
55        derived_notes: list[str] = []
56        if user_facing_message:
57            derived_notes.append(f"user_message={user_facing_message!r}")
58        if meta:
59            derived_notes.append("meta attached (see exception.meta)")
60
61        super().__init__(
62            message=base_message,
63            notes=[*(notes or []), *derived_notes],
64            fatal=fatal,
65        )

Create a block error with optional trace and user-facing context.

Arguments:
  • rail_name: Name of the rail that blocked, if known.
  • reason: Machine-oriented explanation (also embedded in the base message).
  • user_facing_message: Optional short text for clients or UIs.
  • traces: Optional list of ~railtracks.guardrails.core.trace.GuardrailTrace from the failed run.
  • meta: Optional structured details copied from the blocking decision.
  • notes: Extra debug lines forwarded to NodeInvocationError.
  • fatal: Passed through to NodeInvocationError (whether the run is considered unrecoverable).
rail_name
reason
user_facing_message
traces
meta
class GuardrailDecision(pydantic.main.BaseModel):
 26class GuardrailDecision(BaseModel):
 27    """Result of one guardrail invocation.
 28
 29    Which fields are set depends on :attr:`action`:
 30
 31    * ``ALLOW``: only :attr:`reason` and optional :attr:`meta` are typically used.
 32    * ``TRANSFORM``: for input phase, :attr:`messages` holds the new history; for
 33      output phase, :attr:`output_message` holds the new assistant message.
 34    * ``BLOCK``: :attr:`user_facing_message` and :attr:`meta` may carry details for
 35      callers or UIs.
 36    """
 37
 38    model_config = ConfigDict(arbitrary_types_allowed=True)
 39
 40    action: GuardrailAction
 41    reason: str
 42    messages: MessageHistory | None = None
 43    output_message: Message | None = None
 44    user_facing_message: str | None = None
 45    meta: dict[str, Any] | None = None
 46
 47    @classmethod
 48    def allow(
 49        cls, reason: str = "Allowed by guardrail.", meta: dict[str, Any] | None = None
 50    ) -> "GuardrailDecision":
 51        """Build an ``ALLOW`` decision with no content changes.
 52
 53        Args:
 54            reason: Explanation for traces and debugging.
 55            meta: Optional extra fields for observability.
 56
 57        Returns:
 58            A decision with :attr:`action` ``ALLOW``.
 59        """
 60        return cls(action=GuardrailAction.ALLOW, reason=reason, meta=meta)
 61
 62    @classmethod
 63    def block(
 64        cls,
 65        reason: str,
 66        user_facing_message: str | None = None,
 67        meta: dict[str, Any] | None = None,
 68    ) -> "GuardrailDecision":
 69        """Build a ``BLOCK`` decision.
 70
 71        Args:
 72            reason: Explanation for logs, traces, and raised errors.
 73            user_facing_message: Optional message safe to show to end users.
 74            meta: Optional extra fields (e.g. exception details).
 75
 76        Returns:
 77            A decision with :attr:`action` ``BLOCK``.
 78        """
 79        return cls(
 80            action=GuardrailAction.BLOCK,
 81            reason=reason,
 82            user_facing_message=user_facing_message,
 83            meta=meta,
 84        )
 85
 86    @classmethod
 87    def transform_messages(
 88        cls,
 89        messages: MessageHistory,
 90        reason: str,
 91        meta: dict[str, Any] | None = None,
 92    ) -> "GuardrailDecision":
 93        """Build a ``TRANSFORM`` decision for LLM input (message history).
 94
 95        Args:
 96            messages: Replacement conversation history for the model call.
 97            reason: Explanation for traces and debugging.
 98            meta: Optional extra fields (e.g. redaction counts).
 99
100        Returns:
101            A decision with :attr:`action` ``TRANSFORM`` and :attr:`messages` set.
102        """
103        return cls(
104            action=GuardrailAction.TRANSFORM,
105            reason=reason,
106            messages=messages,
107            meta=meta,
108        )
109
110    @classmethod
111    def transform_output(
112        cls,
113        output_message: Message,
114        reason: str,
115        meta: dict[str, Any] | None = None,
116    ) -> "GuardrailDecision":
117        """Build a ``TRANSFORM`` decision for LLM output (assistant message).
118
119        Args:
120            output_message: Replacement assistant message to return.
121            reason: Explanation for traces and debugging.
122            meta: Optional extra fields (e.g. redaction counts).
123
124        Returns:
125            A decision with :attr:`action` ``TRANSFORM`` and :attr:`output_message`
126            set.
127        """
128        return cls(
129            action=GuardrailAction.TRANSFORM,
130            reason=reason,
131            output_message=output_message,
132            meta=meta,
133        )

Result of one guardrail invocation.

Which fields are set depends on action:

model_config = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

action: GuardrailAction
reason: str
output_message: railtracks.llm.Message | None
user_facing_message: str | None
meta: dict[str, typing.Any] | None
@classmethod
def allow( cls, reason: str = 'Allowed by guardrail.', meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
47    @classmethod
48    def allow(
49        cls, reason: str = "Allowed by guardrail.", meta: dict[str, Any] | None = None
50    ) -> "GuardrailDecision":
51        """Build an ``ALLOW`` decision with no content changes.
52
53        Args:
54            reason: Explanation for traces and debugging.
55            meta: Optional extra fields for observability.
56
57        Returns:
58            A decision with :attr:`action` ``ALLOW``.
59        """
60        return cls(action=GuardrailAction.ALLOW, reason=reason, meta=meta)

Build an ALLOW decision with no content changes.

Arguments:
  • reason: Explanation for traces and debugging.
  • meta: Optional extra fields for observability.
Returns:

A decision with action ALLOW.

@classmethod
def block( cls, reason: str, user_facing_message: str | None = None, meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
62    @classmethod
63    def block(
64        cls,
65        reason: str,
66        user_facing_message: str | None = None,
67        meta: dict[str, Any] | None = None,
68    ) -> "GuardrailDecision":
69        """Build a ``BLOCK`` decision.
70
71        Args:
72            reason: Explanation for logs, traces, and raised errors.
73            user_facing_message: Optional message safe to show to end users.
74            meta: Optional extra fields (e.g. exception details).
75
76        Returns:
77            A decision with :attr:`action` ``BLOCK``.
78        """
79        return cls(
80            action=GuardrailAction.BLOCK,
81            reason=reason,
82            user_facing_message=user_facing_message,
83            meta=meta,
84        )

Build a BLOCK decision.

Arguments:
  • reason: Explanation for logs, traces, and raised errors.
  • user_facing_message: Optional message safe to show to end users.
  • meta: Optional extra fields (e.g. exception details).
Returns:

A decision with action BLOCK.

@classmethod
def transform_messages( cls, messages: railtracks.llm.MessageHistory, reason: str, meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
 86    @classmethod
 87    def transform_messages(
 88        cls,
 89        messages: MessageHistory,
 90        reason: str,
 91        meta: dict[str, Any] | None = None,
 92    ) -> "GuardrailDecision":
 93        """Build a ``TRANSFORM`` decision for LLM input (message history).
 94
 95        Args:
 96            messages: Replacement conversation history for the model call.
 97            reason: Explanation for traces and debugging.
 98            meta: Optional extra fields (e.g. redaction counts).
 99
100        Returns:
101            A decision with :attr:`action` ``TRANSFORM`` and :attr:`messages` set.
102        """
103        return cls(
104            action=GuardrailAction.TRANSFORM,
105            reason=reason,
106            messages=messages,
107            meta=meta,
108        )

Build a TRANSFORM decision for LLM input (message history).

Arguments:
  • messages: Replacement conversation history for the model call.
  • reason: Explanation for traces and debugging.
  • meta: Optional extra fields (e.g. redaction counts).
Returns:

A decision with action TRANSFORM and messages set.

@classmethod
def transform_output( cls, output_message: railtracks.llm.Message, reason: str, meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
110    @classmethod
111    def transform_output(
112        cls,
113        output_message: Message,
114        reason: str,
115        meta: dict[str, Any] | None = None,
116    ) -> "GuardrailDecision":
117        """Build a ``TRANSFORM`` decision for LLM output (assistant message).
118
119        Args:
120            output_message: Replacement assistant message to return.
121            reason: Explanation for traces and debugging.
122            meta: Optional extra fields (e.g. redaction counts).
123
124        Returns:
125            A decision with :attr:`action` ``TRANSFORM`` and :attr:`output_message`
126            set.
127        """
128        return cls(
129            action=GuardrailAction.TRANSFORM,
130            reason=reason,
131            output_message=output_message,
132            meta=meta,
133        )

Build a TRANSFORM decision for LLM output (assistant message).

Arguments:
  • output_message: Replacement assistant message to return.
  • reason: Explanation for traces and debugging.
  • meta: Optional extra fields (e.g. redaction counts).
Returns:

A decision with action TRANSFORM and output_message set.

class GuardrailTrace(pydantic.main.BaseModel):
 9class GuardrailTrace(BaseModel):
10    """One guardrail step recorded during a run (for logging or debugging).
11
12    Attributes:
13        rail_name: The guard's :attr:`~railtracks.guardrails.core.interfaces.BaseGuardrail.name`
14            or class name if unset.
15        phase: ``LLMGuardrailPhase`` value string (e.g. ``llm_input``).
16        action: ``allow``, ``transform``, ``block``, or ``error`` when the runner
17            caught an exception, invalid return type, or unknown action.
18        reason: Short explanation, or a fixed message for error traces.
19        meta: Optional details (e.g. :attr:`GuardrailDecision.meta` or exception info).
20    """
21
22    rail_name: str
23    phase: str
24    action: str
25    reason: str
26    meta: dict[str, Any] | None = None

One guardrail step recorded during a run (for logging or debugging).

Attributes:
rail_name: str
phase: str
action: str
reason: str
meta: dict[str, typing.Any] | None
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class BaseLLMGuardrail(railtracks.guardrails.BaseGuardrail):
45class BaseLLMGuardrail(BaseGuardrail):
46    """Abstract base class for guardrails that run on LLM input or output.
47
48    Attributes:
49        phase: Whether this rail expects :class:`LLMGuardrailPhase` ``INPUT`` or
50            ``OUTPUT`` events.
51    """
52
53    phase: LLMGuardrailPhase
54
55    @abstractmethod
56    def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision:
57        pass

Abstract base class for guardrails that run on LLM input or output.

Attributes:
class InputGuard(railtracks.guardrails.BaseLLMGuardrail):
 76class InputGuard(BaseLLMGuardrail):
 77    """Base for guardrails that run on LLM input (e.g. prompt / message history).
 78
 79    Attributes:
 80        phase: Always :attr:`LLMGuardrailPhase.INPUT`.
 81    """
 82
 83    phase = LLMGuardrailPhase.INPUT
 84
 85    def decide(
 86        self,
 87        input: str | Any | MessageHistory | LLMGuardrailEvent,
 88    ) -> GuardrailDecision:
 89        """Run this guard without building an :class:`LLMGuardrailEvent` by hand.
 90
 91        Args:
 92            input: A :class:`LLMGuardrailEvent` (passed through), a ``str`` (treated
 93                as a single user message), a :class:`~railtracks.llm.message.Message`,
 94                or a :class:`~railtracks.llm.history.MessageHistory`.
 95
 96        Returns:
 97            The :class:`GuardrailDecision` from :meth:`__call__`.
 98
 99        Raises:
100            TypeError: If ``input`` is not a ``str``, ``Message``, ``MessageHistory``,
101                or :class:`LLMGuardrailEvent`.
102        """
103        if isinstance(input, LLMGuardrailEvent):
104            return self(input)
105
106        messages = _coerce_to_message_history(input)
107        event = LLMGuardrailEvent(
108            phase=LLMGuardrailPhase.INPUT,
109            messages=messages,
110        )
111        return self(event)

Base for guardrails that run on LLM input (e.g. prompt / message history).

Attributes:
phase = <LLMGuardrailPhase.INPUT: 'llm_input'>
def decide( self, input: Union[str, Any, railtracks.llm.MessageHistory, LLMGuardrailEvent]) -> GuardrailDecision:
 85    def decide(
 86        self,
 87        input: str | Any | MessageHistory | LLMGuardrailEvent,
 88    ) -> GuardrailDecision:
 89        """Run this guard without building an :class:`LLMGuardrailEvent` by hand.
 90
 91        Args:
 92            input: A :class:`LLMGuardrailEvent` (passed through), a ``str`` (treated
 93                as a single user message), a :class:`~railtracks.llm.message.Message`,
 94                or a :class:`~railtracks.llm.history.MessageHistory`.
 95
 96        Returns:
 97            The :class:`GuardrailDecision` from :meth:`__call__`.
 98
 99        Raises:
100            TypeError: If ``input`` is not a ``str``, ``Message``, ``MessageHistory``,
101                or :class:`LLMGuardrailEvent`.
102        """
103        if isinstance(input, LLMGuardrailEvent):
104            return self(input)
105
106        messages = _coerce_to_message_history(input)
107        event = LLMGuardrailEvent(
108            phase=LLMGuardrailPhase.INPUT,
109            messages=messages,
110        )
111        return self(event)

Run this guard without building an LLMGuardrailEvent by hand.

Arguments:
Returns:

The GuardrailDecision from __call__().

Raises:
class OutputGuard(railtracks.guardrails.BaseLLMGuardrail):
114class OutputGuard(BaseLLMGuardrail):
115    """Base for guardrails that run on LLM output (e.g. model response).
116
117    Inspect ``event.output_message`` for the assistant message produced this turn.
118    ``event.messages`` is conversation context and may not yet include that reply.
119
120    Attributes:
121        phase: Always :attr:`LLMGuardrailPhase.OUTPUT`.
122    """
123
124    phase = LLMGuardrailPhase.OUTPUT
125
126    def decide(
127        self,
128        output: str | Any | MessageHistory | LLMGuardrailEvent,
129    ) -> GuardrailDecision:
130        """Run this guard without building an :class:`LLMGuardrailEvent` by hand.
131
132        Args:
133            output: A :class:`LLMGuardrailEvent` (passed through), a ``str`` (becomes
134                the assistant message with empty prior history), a
135                :class:`~railtracks.llm.message.Message`, or a non-empty
136                :class:`~railtracks.llm.history.MessageHistory` (last message is the
137                output under test; earlier entries become ``event.messages``).
138
139        Returns:
140            The :class:`GuardrailDecision` from :meth:`__call__`.
141
142        Raises:
143            ValueError: If ``output`` is an empty :class:`~railtracks.llm.history.MessageHistory`.
144            TypeError: If ``output`` is not a ``str``, ``Message``, ``MessageHistory``,
145                or :class:`LLMGuardrailEvent`.
146        """
147        if isinstance(output, LLMGuardrailEvent):
148            return self(output)
149
150        if isinstance(output, str):
151            output_message = AssistantMessage(output)
152            messages = MessageHistory()
153        elif isinstance(output, Message):
154            output_message = output
155            messages = MessageHistory()
156        elif isinstance(output, MessageHistory):
157            if not output:
158                raise ValueError("Cannot decide with an empty MessageHistory.")
159            output_message = output[-1]
160            messages = MessageHistory(output[:-1])
161        else:
162            raise TypeError(
163                f"Expected str, Message, MessageHistory, or LLMGuardrailEvent, "
164                f"got {type(output).__name__}"
165            )
166
167        event = LLMGuardrailEvent(
168            phase=LLMGuardrailPhase.OUTPUT,
169            messages=messages,
170            output_message=output_message,
171        )
172        return self(event)

Base for guardrails that run on LLM output (e.g. model response).

Inspect event.output_message for the assistant message produced this turn. event.messages is conversation context and may not yet include that reply.

Attributes:
phase = <LLMGuardrailPhase.OUTPUT: 'llm_output'>
def decide( self, output: Union[str, Any, railtracks.llm.MessageHistory, LLMGuardrailEvent]) -> GuardrailDecision:
126    def decide(
127        self,
128        output: str | Any | MessageHistory | LLMGuardrailEvent,
129    ) -> GuardrailDecision:
130        """Run this guard without building an :class:`LLMGuardrailEvent` by hand.
131
132        Args:
133            output: A :class:`LLMGuardrailEvent` (passed through), a ``str`` (becomes
134                the assistant message with empty prior history), a
135                :class:`~railtracks.llm.message.Message`, or a non-empty
136                :class:`~railtracks.llm.history.MessageHistory` (last message is the
137                output under test; earlier entries become ``event.messages``).
138
139        Returns:
140            The :class:`GuardrailDecision` from :meth:`__call__`.
141
142        Raises:
143            ValueError: If ``output`` is an empty :class:`~railtracks.llm.history.MessageHistory`.
144            TypeError: If ``output`` is not a ``str``, ``Message``, ``MessageHistory``,
145                or :class:`LLMGuardrailEvent`.
146        """
147        if isinstance(output, LLMGuardrailEvent):
148            return self(output)
149
150        if isinstance(output, str):
151            output_message = AssistantMessage(output)
152            messages = MessageHistory()
153        elif isinstance(output, Message):
154            output_message = output
155            messages = MessageHistory()
156        elif isinstance(output, MessageHistory):
157            if not output:
158                raise ValueError("Cannot decide with an empty MessageHistory.")
159            output_message = output[-1]
160            messages = MessageHistory(output[:-1])
161        else:
162            raise TypeError(
163                f"Expected str, Message, MessageHistory, or LLMGuardrailEvent, "
164                f"got {type(output).__name__}"
165            )
166
167        event = LLMGuardrailEvent(
168            phase=LLMGuardrailPhase.OUTPUT,
169            messages=messages,
170            output_message=output_message,
171        )
172        return self(event)

Run this guard without building an LLMGuardrailEvent by hand.

Arguments:
Returns:

The GuardrailDecision from __call__().

Raises:
class LLMGuardrailEvent(pydantic.main.BaseModel):
26class LLMGuardrailEvent(BaseModel):
27    """Payload for LLM input and output guardrails.
28
29    Attributes:
30        phase: Whether this is an input-phase or output-phase check.
31        messages: Conversation context, usually the history before the current
32            assistant reply is appended by the node.
33        output_message: For ``OUTPUT`` phase, the assistant :class:`~railtracks.llm.message.Message`
34            under inspection; ``None`` for input phase or when not applicable.
35        node_name: Optional node label for observability.
36        node_uuid: Optional stable node id for observability.
37        run_id: Optional run correlation id.
38        model_name: Optional resolved model name for observability.
39        model_provider: Optional provider string for observability.
40        tags: Optional key/value metadata (e.g. ``agent_kind`` from the mixin).
41    """
42
43    model_config = ConfigDict(arbitrary_types_allowed=True)
44
45    phase: LLMGuardrailPhase
46    messages: MessageHistory
47    output_message: Message | None = None
48
49    node_name: str | None = None
50    node_uuid: str | None = None
51    run_id: str | None = None
52    model_name: str | None = None
53    model_provider: str | None = None
54    tags: dict[str, str] | None = None

Payload for LLM input and output guardrails.

Attributes:
  • phase: Whether this is an input-phase or output-phase check.
  • messages: Conversation context, usually the history before the current assistant reply is appended by the node.
  • output_message: For OUTPUT phase, the assistant ~railtracks.llm.message.Message under inspection; None for input phase or when not applicable.
  • node_name: Optional node label for observability.
  • node_uuid: Optional stable node id for observability.
  • run_id: Optional run correlation id.
  • model_name: Optional resolved model name for observability.
  • model_provider: Optional provider string for observability.
  • tags: Optional key/value metadata (e.g. agent_kind from the mixin).
model_config = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

output_message: railtracks.llm.Message | None
node_name: str | None
node_uuid: str | None
run_id: str | None
model_name: str | None
model_provider: str | None
tags: dict[str, str] | None
class LLMGuardrailPhase(builtins.str, enum.Enum):
14class LLMGuardrailPhase(str, Enum):
15    """Which side of the LLM call a guardrail observes.
16
17    Members:
18        INPUT: Before the model (prompt / history), value ``llm_input``.
19        OUTPUT: After the model (assistant message), value ``llm_output``.
20    """
21
22    INPUT = "llm_input"
23    OUTPUT = "llm_output"

Which side of the LLM call a guardrail observes.

Members:

INPUT: Before the model (prompt / history), value llm_input. OUTPUT: After the model (assistant message), value llm_output.

INPUT = <LLMGuardrailPhase.INPUT: 'llm_input'>
OUTPUT = <LLMGuardrailPhase.OUTPUT: 'llm_output'>