44against input and output data.
55"""
66
7+ import inspect
78from enum import IntEnum
8- from typing import Any
9+ from typing import Any , Callable
910
1011from .guardrails import (
1112 AllFieldsSelector ,
@@ -163,7 +164,7 @@ def format_guardrail_error_message(
163164) -> str :
164165 """Format a guardrail error message following the standard pattern."""
165166 source = "Input" if field_ref .source == FieldSource .INPUT else "Output"
166- message = f"{ source } data didn't match the guardrail condition: [{ field_ref .path } ] { operator } "
167+ message = f"{ source } data didn't match the guardrail condition: [{ field_ref .path } ] comparing function [ { operator } ] "
167168 if expected_value and expected_value .strip ():
168169 message += f" [{ expected_value .strip ()} ]"
169170 return message
@@ -187,16 +188,18 @@ def evaluate_word_rule(
187188 field_str = field_value
188189
189190 # Use the custom function to evaluate the rule
191+ # If detects_violation returns True, it means the rule was violated (validation fails)
190192 try :
191- passed = rule .func (field_str )
193+ violation_detected = rule .detects_violation (field_str )
192194 except Exception :
193195 # If function raises an exception, treat as failure
194- passed = False
196+ violation_detected = True
195197
196- if not passed :
197- reason = format_guardrail_error_message (
198- field_ref , "comparing function" , None
198+ if violation_detected :
199+ operator = (
200+ _humanize_guardrail_func ( rule . detects_violation ) or "violation check"
199201 )
202+ reason = format_guardrail_error_message (field_ref , operator , None )
200203 return False , reason
201204
202205 return True , "All word rule validations passed"
@@ -221,16 +224,18 @@ def evaluate_number_rule(
221224 field_num = float (field_value )
222225
223226 # Use the custom function to evaluate the rule
227+ # If detects_violation returns True, it means the rule was violated (validation fails)
224228 try :
225- passed = rule .func (field_num )
229+ violation_detected = rule .detects_violation (field_num )
226230 except Exception :
227231 # If function raises an exception, treat as failure
228- passed = False
232+ violation_detected = True
229233
230- if not passed :
231- reason = format_guardrail_error_message (
232- field_ref , "comparing function" , None
234+ if violation_detected :
235+ operator = (
236+ _humanize_guardrail_func ( rule . detects_violation ) or "violation check"
233237 )
238+ reason = format_guardrail_error_message (field_ref , operator , None )
234239 return False , reason
235240
236241 return True , "All number rule validations passed"
@@ -256,16 +261,18 @@ def evaluate_boolean_rule(
256261 field_bool = field_value
257262
258263 # Use the custom function to evaluate the rule
264+ # If detects_violation returns True, it means the rule was violated (validation fails)
259265 try :
260- passed = rule .func (field_bool )
266+ violation_detected = rule .detects_violation (field_bool )
261267 except Exception :
262268 # If function raises an exception, treat as failure
263- passed = False
269+ violation_detected = True
264270
265- if not passed :
266- reason = format_guardrail_error_message (
267- field_ref , "comparing function" , None
271+ if violation_detected :
272+ operator = (
273+ _humanize_guardrail_func ( rule . detects_violation ) or "violation check"
268274 )
275+ reason = format_guardrail_error_message (field_ref , operator , None )
269276 return False , reason
270277
271278 return True , "All boolean rule validations passed"
@@ -307,3 +314,72 @@ def evaluate_universal_rule(
307314 return False , "Universal rule validation triggered (input and output)"
308315 else :
309316 return False , f"Unknown apply_to value: { rule .apply_to } "
317+
318+
319+ def _humanize_guardrail_func (func : Callable [..., Any ] | str | None ) -> str | None :
320+ """Build a user-friendly description of a guardrail predicate.
321+
322+ Deterministic guardrails store Python callables (often lambdas) to evaluate
323+ conditions. For diagnostics, it's useful to include a readable hint about the
324+ predicate that failed.
325+
326+ Args:
327+ func: A Python callable used as a predicate, or a pre-rendered string
328+ description (for example, ``"s:str -> bool: contains 'test'"``).
329+
330+ Returns:
331+ A human-readable description, or ``None`` if one cannot be produced.
332+ """
333+ if func is None :
334+ return None
335+
336+ if isinstance (func , str ):
337+ rendered = func .strip ()
338+ return rendered or None
339+
340+ name = getattr (func , "__name__" , None )
341+ if name and name != "<lambda>" :
342+ return name
343+
344+ # Best-effort extraction for lambdas / callables.
345+ try :
346+ sig = str (inspect .signature (func ))
347+ except (TypeError , ValueError ):
348+ sig = ""
349+
350+ try :
351+ source_lines = inspect .getsourcelines (func )
352+ source = "" .join (source_lines [0 ]).strip ()
353+ # Collapse whitespace to keep the message compact.
354+ source = " " .join (source .split ())
355+
356+ # Remove "detects_violation=lambda" prefix if present
357+ # Pattern: "detects_violation=lambda s: condition" -> "condition"
358+ if "detects_violation=lambda" in source :
359+ # Find the lambda part
360+ lambda_start = source .find ("detects_violation=lambda" )
361+ if lambda_start != - 1 :
362+ # Get everything after "detects_violation=lambda"
363+ lambda_part = source [
364+ lambda_start + len ("detects_violation=lambda" ) :
365+ ].strip ()
366+ # Find the colon that separates param from body
367+ colon_idx = lambda_part .find (":" )
368+ if colon_idx != - 1 :
369+ # Extract just the body (condition)
370+ body = lambda_part [colon_idx + 1 :].strip ()
371+ # Remove trailing comma if present
372+ body = body .rstrip ("," ).strip ()
373+ source = body
374+ except (OSError , TypeError ):
375+ source = ""
376+
377+ if source and sig :
378+ return f"{ sig } : { source } "
379+ if source :
380+ return source
381+ if sig :
382+ return sig
383+
384+ rendered = repr (func ).strip ()
385+ return rendered or None
0 commit comments