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

Commit 2d1b5cc

Browse files
Valentina Bojanclaude
andcommitted
fix: pass None to rule evaluation when guardrail references missing field
When a rule references a field that doesn't exist in the data, pass None as the value to detects_violation so the rule lambda can decide the outcome. Previously, missing fields caused the evaluator loop to be skipped entirely, incorrectly treating the rule as violated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fe4f114 commit 2d1b5cc

4 files changed

Lines changed: 130 additions & 30 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.5.4"
3+
version = "0.5.5"
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: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,12 @@ def get_fields_from_selector(
152152
continue
153153
# Extract values (may return multiple if arrays are in the path)
154154
values = extract_field_value(field_ref.path, data)
155-
# Add each value as a separate field reference
156-
for value in values:
157-
fields.append((value, field_ref))
155+
if not values:
156+
# Field is missing from data — pass None so the rule can decide
157+
fields.append((None, field_ref))
158+
else:
159+
for value in values:
160+
fields.append((value, field_ref))
158161

159162
return fields
160163

@@ -199,20 +202,15 @@ def evaluate_word_rule(
199202
field_paths = ", ".join({field_ref.path for _, field_ref in fields})
200203

201204
for field_value, field_ref in fields:
202-
if field_value is None:
203-
continue
204-
205-
# Word rules should only be applied to string values
206-
# Skip non-string values (numbers, booleans, objects, arrays, etc.)
207-
if not isinstance(field_value, str):
205+
# Word rules should only be applied to string values or None (missing field)
206+
# Skip non-string, non-None values (numbers, booleans, objects, arrays, etc.)
207+
if field_value is not None and not isinstance(field_value, str):
208208
continue
209209

210-
field_str = field_value
211-
212210
# Use the custom function to evaluate the rule
213211
# If detects_violation returns True, it means the rule was violated (validation fails)
214212
try:
215-
violation_detected = rule.detects_violation(field_str)
213+
violation_detected = rule.detects_violation(field_value)
216214
except Exception:
217215
# If function raises an exception, treat as failure
218216
violation_detected = True
@@ -240,16 +238,16 @@ def evaluate_number_rule(
240238
operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
241239
field_paths = ", ".join({field_ref.path for _, field_ref in fields})
242240
for field_value, field_ref in fields:
243-
if field_value is None:
244-
continue
245-
246-
# Number rules should only be applied to numeric values
247-
# Skip non-numeric values (strings, booleans, objects, arrays, etc.)
241+
# Number rules should only be applied to numeric values or None (missing field)
242+
# Skip non-numeric, non-None values (strings, booleans, objects, arrays, etc.)
248243
# Note: bool is a subclass of int in Python, so we must check for bool first
249-
if isinstance(field_value, bool) or not isinstance(field_value, (int, float)):
244+
if field_value is not None and (
245+
isinstance(field_value, bool)
246+
or not isinstance(field_value, (int, float))
247+
):
250248
continue
251249

252-
field_num = float(field_value)
250+
field_num = float(field_value) if field_value is not None else None
253251

254252
# Use the custom function to evaluate the rule
255253
# If detects_violation returns True, it means the rule was violated (validation fails)
@@ -284,20 +282,15 @@ def evaluate_boolean_rule(
284282
operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
285283
field_paths = ", ".join({field_ref.path for _, field_ref in fields})
286284
for field_value, field_ref in fields:
287-
if field_value is None:
288-
continue
289-
290-
# Boolean rules should only be applied to boolean values
291-
# Skip non-boolean values (strings, numbers, objects, arrays, etc.)
292-
if not isinstance(field_value, bool):
285+
# Boolean rules should only be applied to boolean values or None (missing field)
286+
# Skip non-boolean, non-None values (strings, numbers, objects, arrays, etc.)
287+
if field_value is not None and not isinstance(field_value, bool):
293288
continue
294289

295-
field_bool = field_value
296-
297290
# Use the custom function to evaluate the rule
298291
# If detects_violation returns True, it means the rule was violated (validation fails)
299292
try:
300-
violation_detected = rule.detects_violation(field_bool)
293+
violation_detected = rule.detects_violation(field_value)
301294
except Exception:
302295
# If function raises an exception, treat as failure
303296
violation_detected = True

tests/guardrails/test_deterministic_guardrails_service.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,3 +1466,110 @@ def _create_guardrail_with_always_rule(
14661466
),
14671467
],
14681468
)
1469+
1470+
1471+
class TestMissingFieldPassesValidation:
1472+
"""Test that rules referencing missing fields pass validation."""
1473+
1474+
def test_word_rule_missing_field_passes(
1475+
self, service: DeterministicGuardrailsService
1476+
) -> None:
1477+
guardrail = DeterministicGuardrail(
1478+
id="test-missing-field",
1479+
name="Missing Field Guardrail",
1480+
description="Test missing field",
1481+
enabled_for_evals=True,
1482+
guardrail_type="custom",
1483+
selector=GuardrailSelector(
1484+
scopes=[GuardrailScope.TOOL], match_names=["test"]
1485+
),
1486+
rules=[
1487+
WordRule(
1488+
rule_type="word",
1489+
field_selector=SpecificFieldsSelector(
1490+
selector_type="specific",
1491+
fields=[
1492+
FieldReference(
1493+
path="sentence2", source=FieldSource.INPUT
1494+
)
1495+
],
1496+
),
1497+
detects_violation=lambda s: len(s or "") > 0,
1498+
rule_description="sentence2 is not empty",
1499+
),
1500+
],
1501+
)
1502+
result = service._evaluate_deterministic_guardrail(
1503+
input_data={"sentence1": "hello"},
1504+
output_data={},
1505+
guardrail=guardrail,
1506+
)
1507+
assert result.result == GuardrailValidationResultType.PASSED
1508+
1509+
def test_number_rule_missing_field_passes(
1510+
self, service: DeterministicGuardrailsService
1511+
) -> None:
1512+
guardrail = DeterministicGuardrail(
1513+
id="test-missing-field",
1514+
name="Missing Field Guardrail",
1515+
description="Test missing field",
1516+
enabled_for_evals=True,
1517+
guardrail_type="custom",
1518+
selector=GuardrailSelector(
1519+
scopes=[GuardrailScope.TOOL], match_names=["test"]
1520+
),
1521+
rules=[
1522+
NumberRule(
1523+
rule_type="number",
1524+
field_selector=SpecificFieldsSelector(
1525+
selector_type="specific",
1526+
fields=[
1527+
FieldReference(path="age", source=FieldSource.INPUT)
1528+
],
1529+
),
1530+
detects_violation=lambda n: n is not None and n < 0,
1531+
rule_description="age is negative",
1532+
),
1533+
],
1534+
)
1535+
result = service._evaluate_deterministic_guardrail(
1536+
input_data={"name": "test"},
1537+
output_data={},
1538+
guardrail=guardrail,
1539+
)
1540+
assert result.result == GuardrailValidationResultType.PASSED
1541+
1542+
def test_boolean_rule_missing_field_passes(
1543+
self, service: DeterministicGuardrailsService
1544+
) -> None:
1545+
guardrail = DeterministicGuardrail(
1546+
id="test-missing-field",
1547+
name="Missing Field Guardrail",
1548+
description="Test missing field",
1549+
enabled_for_evals=True,
1550+
guardrail_type="custom",
1551+
selector=GuardrailSelector(
1552+
scopes=[GuardrailScope.TOOL], match_names=["test"]
1553+
),
1554+
rules=[
1555+
BooleanRule(
1556+
rule_type="boolean",
1557+
field_selector=SpecificFieldsSelector(
1558+
selector_type="specific",
1559+
fields=[
1560+
FieldReference(
1561+
path="is_active", source=FieldSource.INPUT
1562+
)
1563+
],
1564+
),
1565+
detects_violation=lambda b: b is False,
1566+
rule_description="is_active is false",
1567+
),
1568+
],
1569+
)
1570+
result = service._evaluate_deterministic_guardrail(
1571+
input_data={"name": "test"},
1572+
output_data={},
1573+
guardrail=guardrail,
1574+
)
1575+
assert result.result == GuardrailValidationResultType.PASSED

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)