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
21    model_config = ConfigDict(arbitrary_types_allowed=True)
22
23    input: list[InputGuard] = Field(
24        default_factory=list,
25        description="Guardrails run on LLM input (prompt / message history).",
26    )
27    output: list[OutputGuard] = Field(
28        default_factory=list,
29        description="Guardrails run on LLM output (model response).",
30    )
31    tool_call: list[Any] = Field(default_factory=list)
32    tool_response: list[Any] = Field(default_factory=list)
33    fail_open: bool = False
34    trace: bool = True
35
36    @field_validator("input", "output", "tool_call", "tool_response")
37    @classmethod
38    def _validate_callable_rails(
39        cls, value: list[InputGuard | OutputGuard]
40    ) -> list[InputGuard | OutputGuard]:
41        for rail in value:
42            if not callable(rail):
43                raise TypeError(
44                    f"Every guardrail must be callable, got {type(rail).__name__}."
45                )
46        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).

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):
11class Guardrail(Protocol):
12    """
13    Base protocol for all guardrails: callable with a name.
14
15    Concrete ABC hierarchies (e.g. :class:`BaseLLMGuardrail`) narrow the event
16    type and add domain-specific attributes like ``phase``.
17    """
18
19    name: str
20
21    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):
24class BaseGuardrail(ABC):
25    """Abstract base class for all guardrails."""
26
27    name: str
28
29    def __init__(self, name: str | None = None):
30        self.name = name or self.__class__.__name__
31
32    @abstractmethod
33    def __call__(self, event: Any) -> GuardrailDecision:
34        pass

Abstract base class for all guardrails.

name: str
class GuardrailAction(builtins.str, enum.Enum):
12class GuardrailAction(str, Enum):
13    ALLOW = "allow"
14    TRANSFORM = "transform"
15    BLOCK = "block"

An enumeration.

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        self.rail_name = rail_name
32        self.reason = reason
33        self.user_facing_message = user_facing_message
34        self.traces = traces
35        self.meta = meta
36
37        base_message = "Blocked by guardrails"
38        if rail_name:
39            base_message += f" ({rail_name})"
40        base_message += f": {reason}"
41
42        derived_notes: list[str] = []
43        if user_facing_message:
44            derived_notes.append(f"user_message={user_facing_message!r}")
45        if meta:
46            derived_notes.append("meta attached (see exception.meta)")
47
48        super().__init__(
49            message=base_message,
50            notes=[*(notes or []), *derived_notes],
51            fatal=fatal,
52        )

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        self.rail_name = rail_name
32        self.reason = reason
33        self.user_facing_message = user_facing_message
34        self.traces = traces
35        self.meta = meta
36
37        base_message = "Blocked by guardrails"
38        if rail_name:
39            base_message += f" ({rail_name})"
40        base_message += f": {reason}"
41
42        derived_notes: list[str] = []
43        if user_facing_message:
44            derived_notes.append(f"user_message={user_facing_message!r}")
45        if meta:
46            derived_notes.append("meta attached (see exception.meta)")
47
48        super().__init__(
49            message=base_message,
50            notes=[*(notes or []), *derived_notes],
51            fatal=fatal,
52        )
rail_name
reason
user_facing_message
traces
meta
class GuardrailDecision(pydantic.main.BaseModel):
18class GuardrailDecision(BaseModel):
19    model_config = ConfigDict(arbitrary_types_allowed=True)
20
21    action: GuardrailAction
22    reason: str
23    messages: MessageHistory | None = None
24    output_message: Message | None = None
25    user_facing_message: str | None = None
26    meta: dict[str, Any] | None = None
27
28    @classmethod
29    def allow(
30        cls, reason: str = "Allowed by guardrail.", meta: dict[str, Any] | None = None
31    ) -> "GuardrailDecision":
32        return cls(action=GuardrailAction.ALLOW, reason=reason, meta=meta)
33
34    @classmethod
35    def block(
36        cls,
37        reason: str,
38        user_facing_message: str | None = None,
39        meta: dict[str, Any] | None = None,
40    ) -> "GuardrailDecision":
41        return cls(
42            action=GuardrailAction.BLOCK,
43            reason=reason,
44            user_facing_message=user_facing_message,
45            meta=meta,
46        )
47
48    @classmethod
49    def transform_messages(
50        cls,
51        messages: MessageHistory,
52        reason: str,
53        meta: dict[str, Any] | None = None,
54    ) -> "GuardrailDecision":
55        return cls(
56            action=GuardrailAction.TRANSFORM,
57            reason=reason,
58            messages=messages,
59            meta=meta,
60        )
61
62    @classmethod
63    def transform_output(
64        cls,
65        output_message: Message,
66        reason: str,
67        meta: dict[str, Any] | None = None,
68    ) -> "GuardrailDecision":
69        return cls(
70            action=GuardrailAction.TRANSFORM,
71            reason=reason,
72            output_message=output_message,
73            meta=meta,
74        )

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
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:
28    @classmethod
29    def allow(
30        cls, reason: str = "Allowed by guardrail.", meta: dict[str, Any] | None = None
31    ) -> "GuardrailDecision":
32        return cls(action=GuardrailAction.ALLOW, reason=reason, meta=meta)
@classmethod
def block( cls, reason: str, user_facing_message: str | None = None, meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
34    @classmethod
35    def block(
36        cls,
37        reason: str,
38        user_facing_message: str | None = None,
39        meta: dict[str, Any] | None = None,
40    ) -> "GuardrailDecision":
41        return cls(
42            action=GuardrailAction.BLOCK,
43            reason=reason,
44            user_facing_message=user_facing_message,
45            meta=meta,
46        )
@classmethod
def transform_messages( cls, messages: railtracks.llm.MessageHistory, reason: str, meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
48    @classmethod
49    def transform_messages(
50        cls,
51        messages: MessageHistory,
52        reason: str,
53        meta: dict[str, Any] | None = None,
54    ) -> "GuardrailDecision":
55        return cls(
56            action=GuardrailAction.TRANSFORM,
57            reason=reason,
58            messages=messages,
59            meta=meta,
60        )
@classmethod
def transform_output( cls, output_message: railtracks.llm.Message, reason: str, meta: dict[str, typing.Any] | None = None) -> GuardrailDecision:
62    @classmethod
63    def transform_output(
64        cls,
65        output_message: Message,
66        reason: str,
67        meta: dict[str, Any] | None = None,
68    ) -> "GuardrailDecision":
69        return cls(
70            action=GuardrailAction.TRANSFORM,
71            reason=reason,
72            output_message=output_message,
73            meta=meta,
74        )
class GuardrailTrace(pydantic.main.BaseModel):
 9class GuardrailTrace(BaseModel):
10    rail_name: str
11    phase: str
12    action: str
13    reason: str
14    meta: dict[str, Any] | None = None

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
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):
37class BaseLLMGuardrail(BaseGuardrail):
38    """Abstract base class for guardrails that run on LLM input or output."""
39
40    phase: LLMGuardrailPhase
41
42    @abstractmethod
43    def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision:
44        pass

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

class InputGuard(railtracks.guardrails.BaseLLMGuardrail):
47class InputGuard(BaseLLMGuardrail):
48    """Base for guardrails that run on LLM input (e.g. prompt / message history)."""
49
50    phase = LLMGuardrailPhase.INPUT

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

phase = <LLMGuardrailPhase.INPUT: 'llm_input'>
class OutputGuard(railtracks.guardrails.BaseLLMGuardrail):
53class OutputGuard(BaseLLMGuardrail):
54    """
55    Base for guardrails that run on LLM output (e.g. model response).
56
57    Inspect ``event.output_message`` for the assistant message produced this turn.
58    ``event.messages`` is conversation context and may not yet include that reply.
59    """
60
61    phase = LLMGuardrailPhase.OUTPUT

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.

phase = <LLMGuardrailPhase.OUTPUT: 'llm_output'>
class LLMGuardrailEvent(pydantic.main.BaseModel):
19class LLMGuardrailEvent(BaseModel):
20    """
21    Event passed to LLM guardrails.
22
23    - ``messages``: conversation context (usually the history *before* the current
24      assistant reply is appended to the node).
25    - ``output_message``: for ``phase == OUTPUT``, the assistant ``Message`` being
26      guarded this turn; ``None`` for INPUT phase or when not applicable.
27    """
28
29    model_config = ConfigDict(arbitrary_types_allowed=True)
30
31    phase: LLMGuardrailPhase
32    messages: MessageHistory
33    output_message: Message | None = None
34
35    node_name: str | None = None
36    node_uuid: str | None = None
37    run_id: str | None = None
38    model_name: str | None = None
39    model_provider: str | None = None
40    tags: dict[str, str] | None = None

Event passed to LLM guardrails.

  • messages: conversation context (usually the history before the current assistant reply is appended to the node).
  • output_message: for phase == OUTPUT, the assistant Message being guarded this turn; None for INPUT phase or when not applicable.
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    INPUT = "llm_input"
16    OUTPUT = "llm_output"

An enumeration.

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