Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit db612dc

Browse files
Merge pull request #19 from UiPath/AL-229-integrate_deterministic_guardrails
feat: change func behavior [AL-229]
2 parents 7c3e3f3 + c847621 commit db612dc

5 files changed

Lines changed: 194 additions & 55 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-core"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
description = "UiPath Core abstractions"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/core/guardrails/_evaluators.py

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
against input and output data.
55
"""
66

7+
import inspect
78
from enum import IntEnum
8-
from typing import Any
9+
from typing import Any, Callable
910

1011
from .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

src/uipath/core/guardrails/guardrails.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ class WordRule(BaseModel):
9292

9393
rule_type: Literal["word"] = Field(alias="$ruleType")
9494
field_selector: FieldSelector = Field(alias="fieldSelector")
95-
func: Callable[[str], bool] = Field(exclude=True)
95+
detects_violation: Callable[[str], bool] = Field(
96+
exclude=True,
97+
description="Function that returns True if the string violates the rule (validation should fail).",
98+
)
9699

97100
model_config = ConfigDict(populate_by_name=True, extra="allow")
98101

@@ -111,7 +114,10 @@ class NumberRule(BaseModel):
111114

112115
rule_type: Literal["number"] = Field(alias="$ruleType")
113116
field_selector: FieldSelector = Field(alias="fieldSelector")
114-
func: Callable[[float], bool] = Field(exclude=True)
117+
detects_violation: Callable[[float], bool] = Field(
118+
exclude=True,
119+
description="Function that returns True if the number violates the rule (validation should fail).",
120+
)
115121

116122
model_config = ConfigDict(populate_by_name=True, extra="allow")
117123

@@ -121,7 +127,10 @@ class BooleanRule(BaseModel):
121127

122128
rule_type: Literal["boolean"] = Field(alias="$ruleType")
123129
field_selector: FieldSelector = Field(alias="fieldSelector")
124-
func: Callable[[bool], bool] = Field(exclude=True)
130+
detects_violation: Callable[[bool], bool] = Field(
131+
exclude=True,
132+
description="Function that returns True if the boolean violates the rule (validation should fail).",
133+
)
125134

126135
model_config = ConfigDict(populate_by_name=True, extra="allow")
127136

@@ -164,7 +173,7 @@ class BaseGuardrail(BaseModel):
164173
class DeterministicGuardrail(BaseGuardrail):
165174
"""Deterministic guardrail model."""
166175

167-
guardrail_type: Literal["custom"] = Field(alias="custom")
176+
guardrail_type: Literal["custom"] = Field(alias="$guardrailType")
168177
rules: list[Rule]
169178

170179
model_config = ConfigDict(populate_by_name=True, extra="allow")

0 commit comments

Comments
 (0)