railtracks.prebuilt.guardrails
1from railtracks.guardrails.llm._pii.config import ( 2 PIICustomPattern, 3 PIIEntity, 4 PIIRedactConfig, 5) 6from railtracks.guardrails.llm.input.block_text import BlockTextInputGuard 7from railtracks.guardrails.llm.input.length_guard import InputLengthGuard 8from railtracks.guardrails.llm.input.pii_redact import PIIRedactInputGuard 9from railtracks.guardrails.llm.output.block_text import BlockTextOutputGuard 10from railtracks.guardrails.llm.output.length_guard import OutputLengthGuard 11from railtracks.guardrails.llm.output.pii_redact import PIIRedactOutputGuard 12 13__all__ = [ 14 "BlockTextInputGuard", 15 "BlockTextOutputGuard", 16 "InputLengthGuard", 17 "OutputLengthGuard", 18 "PIICustomPattern", 19 "PIIEntity", 20 "PIIRedactConfig", 21 "PIIRedactInputGuard", 22 "PIIRedactOutputGuard", 23]
14class BlockTextInputGuard(InputGuard): 15 """Blocks LLM input when any user or system message matches a regex pattern.""" 16 17 def __init__( 18 self, 19 pattern: str, 20 *, 21 name: str | None = None, 22 user_facing_message: str | None = None, 23 ) -> None: 24 """Initialize the input block-text guard. 25 26 Args: 27 pattern: Regex pattern; if it matches any scannable message content 28 the guard returns ``BLOCK``. 29 name: Optional rail name for traces (see :class:`InputGuard`). 30 user_facing_message: Optional message surfaced to UIs and 31 visualizers when the guard blocks. 32 33 Raises: 34 re.error: If *pattern* is not a valid regular expression. 35 """ 36 super().__init__(name=name) 37 self._pattern = re.compile(pattern) 38 self._user_facing_message = user_facing_message 39 40 def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision: 41 """Block if any user/system string message matches the pattern. 42 43 Returns: 44 ``BLOCK`` when the pattern is found, ``ALLOW`` otherwise. 45 """ 46 for msg in event.messages: 47 if msg.role not in _SCANNABLE_ROLES or not isinstance(msg.content, str): 48 continue 49 if self._pattern.search(msg.content): 50 return GuardrailDecision.block( 51 reason=("Input blocked: prohibited content detected."), 52 user_facing_message=self._user_facing_message, 53 ) 54 return GuardrailDecision.allow(reason="No blocked patterns detected in input.")
Blocks LLM input when any user or system message matches a regex pattern.
17 def __init__( 18 self, 19 pattern: str, 20 *, 21 name: str | None = None, 22 user_facing_message: str | None = None, 23 ) -> None: 24 """Initialize the input block-text guard. 25 26 Args: 27 pattern: Regex pattern; if it matches any scannable message content 28 the guard returns ``BLOCK``. 29 name: Optional rail name for traces (see :class:`InputGuard`). 30 user_facing_message: Optional message surfaced to UIs and 31 visualizers when the guard blocks. 32 33 Raises: 34 re.error: If *pattern* is not a valid regular expression. 35 """ 36 super().__init__(name=name) 37 self._pattern = re.compile(pattern) 38 self._user_facing_message = user_facing_message
Initialize the input block-text guard.
Arguments:
- pattern: Regex pattern; if it matches any scannable message content
the guard returns
BLOCK. - name: Optional rail name for traces (see
InputGuard). - user_facing_message: Optional message surfaced to UIs and visualizers when the guard blocks.
Raises:
- re.error: If pattern is not a valid regular expression.
11class BlockTextOutputGuard(OutputGuard): 12 """Blocks LLM output when the assistant message matches a regex pattern.""" 13 14 def __init__( 15 self, 16 pattern: str, 17 *, 18 name: str | None = None, 19 user_facing_message: str | None = None, 20 ) -> None: 21 """Initialize the output block-text guard. 22 23 Args: 24 pattern: Regex pattern; if it matches the output message content 25 the guard returns ``BLOCK``. 26 name: Optional rail name for traces (see :class:`OutputGuard`). 27 user_facing_message: Optional message surfaced to UIs and 28 visualizers when the guard blocks. 29 30 Raises: 31 re.error: If *pattern* is not a valid regular expression. 32 """ 33 super().__init__(name=name) 34 self._pattern = re.compile(pattern) 35 self._user_facing_message = user_facing_message 36 37 def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision: 38 """Block if the output message matches the pattern. 39 40 Returns: 41 ``BLOCK`` when the pattern is found, ``ALLOW`` otherwise. 42 """ 43 msg = event.output_message 44 if msg is None or not isinstance(msg.content, str): 45 return GuardrailDecision.allow(reason="No string output to scan.") 46 47 if self._pattern.search(msg.content): 48 return GuardrailDecision.block( 49 reason=("Output blocked: prohibited content detected."), 50 user_facing_message=self._user_facing_message, 51 ) 52 return GuardrailDecision.allow(reason="No blocked patterns detected in output.")
Blocks LLM output when the assistant message matches a regex pattern.
14 def __init__( 15 self, 16 pattern: str, 17 *, 18 name: str | None = None, 19 user_facing_message: str | None = None, 20 ) -> None: 21 """Initialize the output block-text guard. 22 23 Args: 24 pattern: Regex pattern; if it matches the output message content 25 the guard returns ``BLOCK``. 26 name: Optional rail name for traces (see :class:`OutputGuard`). 27 user_facing_message: Optional message surfaced to UIs and 28 visualizers when the guard blocks. 29 30 Raises: 31 re.error: If *pattern* is not a valid regular expression. 32 """ 33 super().__init__(name=name) 34 self._pattern = re.compile(pattern) 35 self._user_facing_message = user_facing_message
Initialize the output block-text guard.
Arguments:
- pattern: Regex pattern; if it matches the output message content
the guard returns
BLOCK. - name: Optional rail name for traces (see
OutputGuard). - user_facing_message: Optional message surfaced to UIs and visualizers when the guard blocks.
Raises:
- re.error: If pattern is not a valid regular expression.
11class InputLengthGuard(InputGuard): 12 """Blocks LLM input (the full message history) that exceeds ``max_chars`` characters. 13 14 Character counting is used as the simplest, dependency-free unit. A future 15 implementation may add word- or token-based counting via an optional parameter. 16 17 Example:: 18 19 guard = InputLengthGuard(max_chars=4000) 20 21 Args: 22 max_chars: Maximum number of characters allowed across all messages in the 23 input history. Defaults to ``4096``. 24 name: Optional display name for the guardrail instance. 25 """ 26 27 def __init__(self, max_chars: int = 4096, name: str | None = None) -> None: 28 super().__init__(name=name) 29 if max_chars <= 0: 30 raise ValueError(f"max_chars must be a positive integer, got {max_chars!r}") 31 self.max_chars = max_chars 32 33 def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision: 34 total_chars = sum(len(m.content or "") for m in event.messages) 35 if total_chars > self.max_chars: 36 return GuardrailDecision.block( 37 reason=( 38 f"Input length {total_chars} characters exceeds the maximum of " 39 f"{self.max_chars} characters." 40 ), 41 user_facing_message=( 42 "Your message is too long. Please shorten your input and try again." 43 ), 44 meta={"total_chars": total_chars, "max_chars": self.max_chars}, 45 ) 46 return GuardrailDecision.allow( 47 reason=f"Input length {total_chars} chars is within the {self.max_chars}-char limit.", 48 meta={"total_chars": total_chars, "max_chars": self.max_chars}, 49 )
Blocks LLM input (the full message history) that exceeds max_chars characters.
Character counting is used as the simplest, dependency-free unit. A future implementation may add word- or token-based counting via an optional parameter.
Example::
guard = InputLengthGuard(max_chars=4000)
Arguments:
- max_chars: Maximum number of characters allowed across all messages in the
input history. Defaults to
4096. - name: Optional display name for the guardrail instance.
27 def __init__(self, max_chars: int = 4096, name: str | None = None) -> None: 28 super().__init__(name=name) 29 if max_chars <= 0: 30 raise ValueError(f"max_chars must be a positive integer, got {max_chars!r}") 31 self.max_chars = max_chars
Initialize the guardrail.
Arguments:
- name: Rail name for traces and debugging; defaults to the class name.
11class OutputLengthGuard(OutputGuard): 12 """Blocks LLM output that exceeds ``max_chars`` characters. 13 14 Inspects ``event.output_message`` (the assistant reply produced this turn). 15 16 Example:: 17 18 guard = OutputLengthGuard(max_chars=2000) 19 20 Args: 21 max_chars: Maximum number of characters allowed in the assistant reply. 22 Defaults to ``2048``. 23 name: Optional display name for the guardrail instance. 24 """ 25 26 def __init__(self, max_chars: int = 2048, name: str | None = None) -> None: 27 super().__init__(name=name) 28 if max_chars <= 0: 29 raise ValueError(f"max_chars must be a positive integer, got {max_chars!r}") 30 self.max_chars = max_chars 31 32 def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision: 33 if event.output_message is None: 34 return GuardrailDecision.allow(reason="No output message to evaluate.") 35 36 content = event.output_message.content or "" 37 total_chars = len(content) 38 if total_chars > self.max_chars: 39 return GuardrailDecision.block( 40 reason=( 41 f"Output length {total_chars} characters exceeds the maximum of " 42 f"{self.max_chars} characters." 43 ), 44 user_facing_message=( 45 "The response was too long and has been blocked. " 46 "Please try a more specific question." 47 ), 48 meta={"total_chars": total_chars, "max_chars": self.max_chars}, 49 ) 50 return GuardrailDecision.allow( 51 reason=f"Output length {total_chars} chars is within the {self.max_chars}-char limit.", 52 meta={"total_chars": total_chars, "max_chars": self.max_chars}, 53 )
Blocks LLM output that exceeds max_chars characters.
Inspects event.output_message (the assistant reply produced this turn).
Example::
guard = OutputLengthGuard(max_chars=2000)
Arguments:
- max_chars: Maximum number of characters allowed in the assistant reply.
Defaults to
2048. - name: Optional display name for the guardrail instance.
26 def __init__(self, max_chars: int = 2048, name: str | None = None) -> None: 27 super().__init__(name=name) 28 if max_chars <= 0: 29 raise ValueError(f"max_chars must be a positive integer, got {max_chars!r}") 30 self.max_chars = max_chars
Initialize the guardrail.
Arguments:
- name: Rail name for traces and debugging; defaults to the class name.
43class PIICustomPattern(BaseModel): 44 """ 45 User-defined PII pattern. 46 47 ``name`` becomes the placeholder label: e.g. ``"EMPLOYEE_ID"`` yields 48 ``[EMPLOYEE_ID]`` in redacted text. 49 50 Attributes: 51 name: Label used in placeholders and metadata. 52 regex: Pattern passed to :func:`re.compile` for matching. 53 """ 54 55 model_config = ConfigDict(frozen=True) 56 57 name: str 58 regex: str
User-defined PII pattern.
name becomes the placeholder label: e.g. "EMPLOYEE_ID" yields
[EMPLOYEE_ID] in redacted text.
Attributes:
- name: Label used in placeholders and metadata.
- regex: Pattern passed to
re.compile()for matching.
9class PIIEntity(str, Enum): 10 """Built-in PII entity types with reliable regex detection.""" 11 12 EMAIL_ADDRESS = "EMAIL_ADDRESS" 13 PHONE_NUMBER = "PHONE_NUMBER" 14 CREDIT_CARD = "CREDIT_CARD" 15 US_SSN = "US_SSN" 16 CA_SIN = "CA_SIN" 17 IP_ADDRESS = "IP_ADDRESS" 18 URL = "URL" 19 IBAN_CODE = "IBAN_CODE" 20 21 @classmethod 22 def available(cls) -> dict[str, str]: 23 """Return built-in entity codes and short descriptions for UI or docs. 24 25 Returns: 26 Mapping from entity value string (e.g. ``EMAIL_ADDRESS``) to description. 27 """ 28 return {e.value: _ENTITY_DESCRIPTIONS[e] for e in cls}
Built-in PII entity types with reliable regex detection.
21 @classmethod 22 def available(cls) -> dict[str, str]: 23 """Return built-in entity codes and short descriptions for UI or docs. 24 25 Returns: 26 Mapping from entity value string (e.g. ``EMAIL_ADDRESS``) to description. 27 """ 28 return {e.value: _ENTITY_DESCRIPTIONS[e] for e in cls}
Return built-in entity codes and short descriptions for UI or docs.
Returns:
Mapping from entity value string (e.g.
EMAIL_ADDRESS) to description.
61class PIIRedactConfig(BaseModel): 62 """ 63 Configuration for PII redaction guardrails. 64 65 Frozen so a single instance can safely be shared between input and output 66 guard instances. 67 68 Attributes: 69 entities: Built-in :class:`PIIEntity` kinds to detect; defaults to all members. 70 custom_patterns: Extra :class:`PIICustomPattern` rows merged into detection. 71 """ 72 73 model_config = ConfigDict(frozen=True) 74 75 entities: list[PIIEntity] = list(PIIEntity) 76 custom_patterns: list[PIICustomPattern] = []
Configuration for PII redaction guardrails.
Frozen so a single instance can safely be shared between input and output guard instances.
Attributes:
- entities: Built-in
PIIEntitykinds to detect; defaults to all members. - custom_patterns: Extra
PIICustomPatternrows merged into detection.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
18class PIIRedactInputGuard(InputGuard): 19 """Redacts PII from user and system string messages before they reach the LLM.""" 20 21 def __init__( 22 self, 23 config: PIIRedactConfig | None = None, 24 *, 25 name: str | None = None, 26 ) -> None: 27 """Initialize the input PII redactor. 28 29 Args: 30 config: Redaction settings; defaults to all built-in entity kinds and no 31 custom patterns (see :class:`~railtracks.guardrails.llm.PIIRedactConfig`). 32 name: Optional rail name for traces (see :class:`InputGuard`). 33 """ 34 super().__init__(name=name) 35 self._config = config or PIIRedactConfig() 36 self._engine = PIIEngine(self._config) 37 38 def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision: 39 """Scan user/system string content and redact matches. 40 41 Returns: 42 ``ALLOW`` when no PII is found, or ``TRANSFORM`` with rewritten messages on 43 :attr:`~railtracks.guardrails.core.decision.GuardrailDecision.messages` 44 and redaction metadata in ``meta``. 45 """ 46 all_records: list[RedactionRecord] = [] 47 new_messages: list[Message] = [] 48 messages_affected = 0 49 50 for msg in event.messages: 51 if msg.role not in _SCANNABLE_ROLES or not isinstance(msg.content, str): 52 new_messages.append(msg) 53 continue 54 55 redacted_text, records = self._engine.redact(msg.content) 56 if records: 57 all_records.extend(records) 58 messages_affected += 1 59 clone = deepcopy(msg) 60 clone._content = redacted_text 61 new_messages.append(clone) 62 else: 63 new_messages.append(msg) 64 65 if not all_records: 66 return GuardrailDecision.allow(reason="No PII detected in input.") 67 68 return GuardrailDecision.transform_messages( 69 messages=MessageHistory(new_messages), 70 reason=f"Redacted {len(all_records)} PII span(s) from input messages.", 71 meta=build_redaction_meta(all_records, messages_affected=messages_affected), 72 )
Redacts PII from user and system string messages before they reach the LLM.
21 def __init__( 22 self, 23 config: PIIRedactConfig | None = None, 24 *, 25 name: str | None = None, 26 ) -> None: 27 """Initialize the input PII redactor. 28 29 Args: 30 config: Redaction settings; defaults to all built-in entity kinds and no 31 custom patterns (see :class:`~railtracks.guardrails.llm.PIIRedactConfig`). 32 name: Optional rail name for traces (see :class:`InputGuard`). 33 """ 34 super().__init__(name=name) 35 self._config = config or PIIRedactConfig() 36 self._engine = PIIEngine(self._config)
Initialize the input PII redactor.
Arguments:
- config: Redaction settings; defaults to all built-in entity kinds and no
custom patterns (see
~railtracks.guardrails.llm.PIIRedactConfig). - name: Optional rail name for traces (see
InputGuard).
14class PIIRedactOutputGuard(OutputGuard): 15 """Redacts PII from the assistant string response after LLM generation.""" 16 17 def __init__( 18 self, 19 config: PIIRedactConfig | None = None, 20 *, 21 name: str | None = None, 22 ) -> None: 23 """Initialize the output PII redactor. 24 25 Args: 26 config: Which built-in entities and custom patterns to apply; defaults to 27 all built-in entity kinds. 28 name: Optional rail name for traces (see :class:`OutputGuard`). 29 """ 30 super().__init__(name=name) 31 self._config = config or PIIRedactConfig() 32 self._engine = PIIEngine(self._config) 33 34 def __call__(self, event: LLMGuardrailEvent) -> GuardrailDecision: 35 """Redact PII from string assistant content on ``event.output_message``. 36 37 Returns: 38 ``ALLOW`` when there is nothing to scan or no PII, or ``TRANSFORM`` with the 39 rewritten message on 40 :attr:`~railtracks.guardrails.core.decision.GuardrailDecision.output_message` 41 and redaction metadata in ``meta``. 42 """ 43 msg = event.output_message 44 if msg is None or not isinstance(msg.content, str): 45 return GuardrailDecision.allow(reason="No string output to scan.") 46 47 redacted_text, records = self._engine.redact(msg.content) 48 if not records: 49 return GuardrailDecision.allow(reason="No PII detected in output.") 50 51 clone = deepcopy(msg) 52 clone._content = redacted_text 53 return GuardrailDecision.transform_output( 54 output_message=clone, 55 reason=f"Redacted {len(records)} PII span(s) from output.", 56 meta=build_redaction_meta(records), 57 )
Redacts PII from the assistant string response after LLM generation.
17 def __init__( 18 self, 19 config: PIIRedactConfig | None = None, 20 *, 21 name: str | None = None, 22 ) -> None: 23 """Initialize the output PII redactor. 24 25 Args: 26 config: Which built-in entities and custom patterns to apply; defaults to 27 all built-in entity kinds. 28 name: Optional rail name for traces (see :class:`OutputGuard`). 29 """ 30 super().__init__(name=name) 31 self._config = config or PIIRedactConfig() 32 self._engine = PIIEngine(self._config)
Initialize the output PII redactor.
Arguments:
- config: Which built-in entities and custom patterns to apply; defaults to all built-in entity kinds.
- name: Optional rail name for traces (see
OutputGuard).