railtracks.guardrails.llm

LLM-level guardrails.

The concrete prebuilt guards and the PII configuration classes are relocating to rt.prebuilt.guardrails in railtracks 1.5.0, where they are already available today. Accessing them from here still works in this release but emits a FutureWarning.

The authoring bases InputGuard / OutputGuard and the decision types are not moving; keep importing those from rt.guardrails.

 1"""LLM-level guardrails.
 2
 3The concrete prebuilt guards and the PII configuration classes are **relocating** to
 4``rt.prebuilt.guardrails`` in railtracks 1.5.0, where they are already available today.
 5Accessing them from here still works in this release but emits a ``FutureWarning``.
 6
 7The authoring bases ``InputGuard`` / ``OutputGuard`` and the decision types are not
 8moving; keep importing those from ``rt.guardrails``.
 9"""
10
11from __future__ import annotations
12
13import importlib
14from typing import TYPE_CHECKING
15
16from railtracks.utils.deprecation import warn_pending_change
17
18from .mixin import LLMGuardrailsMixin
19
20if TYPE_CHECKING:
21    # Redundant aliases mark these as intentional re-exports, so type checkers still
22    # resolve the deprecated spellings even though `__all__` no longer advertises them.
23    from . import input as input
24    from . import output as output
25    from ._pii.config import PIICustomPattern as PIICustomPattern
26    from ._pii.config import PIIEntity as PIIEntity
27    from ._pii.config import PIIRedactConfig as PIIRedactConfig
28    from .input.block_text import BlockTextInputGuard as BlockTextInputGuard
29    from .input.length_guard import InputLengthGuard as InputLengthGuard
30    from .input.pii_redact import PIIRedactInputGuard as PIIRedactInputGuard
31    from .output.block_text import BlockTextOutputGuard as BlockTextOutputGuard
32    from .output.length_guard import OutputLengthGuard as OutputLengthGuard
33    from .output.pii_redact import PIIRedactOutputGuard as PIIRedactOutputGuard
34
35# The relocated names now live in `rt.prebuilt.guardrails`
36__all__ = [
37    "LLMGuardrailsMixin",
38]
39
40# name -> module (relative to this package) it is defined in.
41_RELOCATED: dict[str, str] = {
42    "BlockTextInputGuard": ".input.block_text",
43    "InputLengthGuard": ".input.length_guard",
44    "PIIRedactInputGuard": ".input.pii_redact",
45    "BlockTextOutputGuard": ".output.block_text",
46    "OutputLengthGuard": ".output.length_guard",
47    "PIIRedactOutputGuard": ".output.pii_redact",
48    "PIICustomPattern": "._pii.config",
49    "PIIEntity": "._pii.config",
50    "PIIRedactConfig": "._pii.config",
51}
52
53
54def __getattr__(name: str):
55    if name in _RELOCATED:
56        warn_pending_change(
57            f"rt.guardrails.llm.{name}",
58            change="moves",
59            instead=f"rt.prebuilt.guardrails.{name}",
60            detail="The class itself is unchanged.",
61        )
62        module = importlib.import_module(_RELOCATED[name], __name__)
63        return getattr(module, name)
64
65    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
66
67
68def __dir__() -> list[str]:
69    return sorted([*__all__, *_RELOCATED])
class LLMGuardrailsMixin:
 25class LLMGuardrailsMixin:
 26    """Mixin for nodes that invoke an LLM.
 27
 28    Overrides ``_pre_invoke`` and ``_post_invoke`` to run input and output guardrails.
 29    Set ``guardrails=`` when building the node. Expects ``self._details`` to contain a
 30    ``guard_details`` list; each run extends it with :class:`~railtracks.guardrails.core.trace.GuardrailTrace`
 31    rows when rails execute.
 32
 33    Output rails run only when ``_post_invoke`` receives a :class:`~railtracks.llm.response.Response`;
 34    other result types are returned unchanged.
 35
 36    Raises:
 37        GuardrailBlockedError: When a rail returns ``BLOCK`` (runner stops the chain
 38            with a blocking decision).
 39    """
 40
 41    guardrails: Guard | None = None
 42    _details: DebugDetails
 43    llm_model: ModelBase
 44    uuid: str
 45    name: Callable[[], str]
 46
 47    def _append_guard_traces(self, traces: list[GuardrailTrace]) -> None:
 48        if not traces:
 49            return
 50        self._details["guard_details"].extend(traces)
 51
 52    def _guardrail_agent_kind(self) -> str:
 53        cls_name = self.__class__.__name__.lower()
 54        if "structured" in cls_name and "toolcall" in cls_name:
 55            return "structured_tool_call"
 56        if "toolcall" in cls_name:
 57            return "tool_call"
 58        if "structured" in cls_name:
 59            return "structured"
 60        if "terminal" in cls_name:
 61            return "terminal"
 62        return "llm"
 63
 64    def _resolve_model_metadata(self) -> tuple[str | None, str | None]:
 65        model_name = getattr(self.llm_model, "model_name", None)
 66        if callable(model_name):
 67            model_name = model_name()
 68        model_provider = getattr(self.llm_model, "model_provider", None)
 69        if callable(model_provider):
 70            model_provider = model_provider()
 71        return (
 72            cast(str | None, model_name),
 73            str(model_provider) if model_provider is not None else None,
 74        )
 75
 76    def _build_input_event(self, context: Any) -> LLMGuardrailEvent:
 77        """Build LLMGuardrailEvent for input phase from context (MessageHistory)."""
 78        model_name, model_provider = self._resolve_model_metadata()
 79        return LLMGuardrailEvent(
 80            phase=LLMGuardrailPhase.INPUT,
 81            messages=context,
 82            node_name=self.name(),
 83            node_uuid=self.uuid,
 84            model_name=model_name,
 85            model_provider=model_provider,
 86            tags={"agent_kind": self._guardrail_agent_kind()},
 87        )
 88
 89    def _build_output_event(
 90        self, context: Any, assistant_message: Message
 91    ) -> LLMGuardrailEvent:
 92        """Build LLMGuardrailEvent for output phase: context is message history; assistant_message is this turn's output."""
 93        model_name, model_provider = self._resolve_model_metadata()
 94        return LLMGuardrailEvent(
 95            phase=LLMGuardrailPhase.OUTPUT,
 96            messages=context,
 97            output_message=assistant_message,
 98            node_name=self.name(),
 99            node_uuid=self.uuid,
100            model_name=model_name,
101            model_provider=model_provider,
102            tags={"agent_kind": self._guardrail_agent_kind()},
103        )
104
105    def _pre_invoke(self, context: Any) -> Any:
106        if self.guardrails is None or not self.guardrails.input:
107            return context
108        event = self._build_input_event(context)
109        new_context, traces, decision = GuardRunner(self.guardrails).run_llm_input(
110            event
111        )
112        self._append_guard_traces(traces)
113        if decision is not None and decision.action == GuardrailAction.BLOCK:
114            rail_name = traces[-1].rail_name if traces else None
115            raise GuardrailBlockedError(
116                rail_name=rail_name,
117                reason=decision.reason,
118                user_facing_message=decision.user_facing_message,
119                traces=traces,
120                meta=decision.meta,
121            )
122
123        return new_context
124
125    def _post_invoke(self, context: Any, result: Any) -> Any:
126        if self.guardrails is None or not self.guardrails.output:
127            return result
128        if not isinstance(result, Response):
129            return result
130        event = self._build_output_event(context, result.message)
131        new_message, traces, decision = GuardRunner(self.guardrails).run_llm_output(
132            event, result.message
133        )
134        self._append_guard_traces(traces)
135        if decision is not None and decision.action == GuardrailAction.BLOCK:
136            rail_name = traces[-1].rail_name if traces else None
137            raise GuardrailBlockedError(
138                rail_name=rail_name,
139                reason=decision.reason,
140                user_facing_message=decision.user_facing_message,
141                traces=traces,
142                meta=decision.meta,
143            )
144
145        return Response(message=new_message, message_info=result.message_info)

Mixin for nodes that invoke an LLM.

Overrides _pre_invoke and _post_invoke to run input and output guardrails. Set guardrails= when building the node. Expects self._details to contain a guard_details list; each run extends it with ~railtracks.guardrails.core.trace.GuardrailTrace rows when rails execute.

Output rails run only when _post_invoke receives a ~railtracks.llm.response.Response; other result types are returned unchanged.

Raises:
  • GuardrailBlockedError: When a rail returns BLOCK (runner stops the chain with a blocking decision).
guardrails: railtracks.guardrails.core.config.Guard | None = None
uuid: str
name: Callable[[], str]