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]
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.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
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.
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)
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.
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.
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.
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.
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.GuardrailTracefrom 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).
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:
ALLOW: onlyreasonand optionalmetaare typically used.TRANSFORM: for input phase,messagesholds the new history; for output phase,output_messageholds the new assistant message.BLOCK:user_facing_messageandmetamay carry details for callers or UIs.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
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
actionALLOW.
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
actionBLOCK.
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 )
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
actionTRANSFORMandoutput_messageset.
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: The guard's
~railtracks.guardrails.core.interfaces.BaseGuardrail.nameor class name if unset. - phase:
LLMGuardrailPhasevalue string (e.g.llm_input). - action:
allow,transform,block, orerrorwhen the runner caught an exception, invalid return type, or unknown action. - reason: Short explanation, or a fixed message for error traces.
- meta: Optional details (e.g.
GuardrailDecision.metaor exception info).
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:
- phase: Whether this rail expects
LLMGuardrailPhaseINPUTorOUTPUTevents.
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: Always
LLMGuardrailPhase.INPUT.
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:
- input: A
LLMGuardrailEvent(passed through), astr(treated as a single user message), a~railtracks.llm.message.Message, or a~railtracks.llm.history.MessageHistory.
Returns:
The
GuardrailDecisionfrom__call__().
Raises:
- TypeError: If
inputis not astr,Message,MessageHistory, orLLMGuardrailEvent.
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: Always
LLMGuardrailPhase.OUTPUT.
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:
- output: A
LLMGuardrailEvent(passed through), astr(becomes the assistant message with empty prior history), a~railtracks.llm.message.Message, or a non-empty~railtracks.llm.history.MessageHistory(last message is the output under test; earlier entries becomeevent.messages).
Returns:
The
GuardrailDecisionfrom__call__().
Raises:
- ValueError: If
outputis an empty~railtracks.llm.history.MessageHistory. - TypeError: If
outputis not astr,Message,MessageHistory, orLLMGuardrailEvent.
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
OUTPUTphase, the assistant~railtracks.llm.message.Messageunder inspection;Nonefor 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_kindfrom the mixin).
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
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), valuellm_output.