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

Commit 3af69fd

Browse files
Valentina Bojanclaude
andcommitted
fix: pass validation when guardrail rule references missing field
When a rule references a field that doesn't exist in the data, the evaluator should pass (nothing to validate) instead of failing. Previously, the empty field list caused the loop to be skipped and fall through to the "violation detected" return. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fe4f114 commit 3af69fd

4 files changed

Lines changed: 123 additions & 8 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ def evaluate_word_rule(
195195
) -> tuple[bool, str]:
196196
"""Evaluate a word rule against input and output data."""
197197
fields = get_fields_from_selector(rule.field_selector, input_data, output_data)
198+
if not fields:
199+
return True, "No fields to validate"
200+
198201
operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
199202
field_paths = ", ".join({field_ref.path for _, field_ref in fields})
200203

@@ -237,6 +240,9 @@ def evaluate_number_rule(
237240
) -> tuple[bool, str]:
238241
"""Evaluate a number rule against input and output data."""
239242
fields = get_fields_from_selector(rule.field_selector, input_data, output_data)
243+
if not fields:
244+
return True, "No fields to validate"
245+
240246
operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
241247
field_paths = ", ".join({field_ref.path for _, field_ref in fields})
242248
for field_value, field_ref in fields:
@@ -281,6 +287,9 @@ def evaluate_boolean_rule(
281287
) -> tuple[bool, str]:
282288
"""Evaluate a boolean rule against input and output data."""
283289
fields = get_fields_from_selector(rule.field_selector, input_data, output_data)
290+
if not fields:
291+
return True, "No fields to validate"
292+
284293
operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
285294
field_paths = ", ".join({field_ref.path for _, field_ref in fields})
286295
for field_value, field_ref in fields:

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: s == "",
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 < 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

tests/guardrails/test_guardrails_models.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ def test_field_reference_normalizes_capitalized_source(
2020
) -> None:
2121
"""Test that FieldReference normalizes capitalized source values to lowercase."""
2222
# Create FieldReference with capitalized "Input" - should normalize to FieldSource.INPUT
23-
field_ref = FieldReference(path="testField", source="Input") # type: ignore[arg-type]
23+
field_ref = FieldReference(path="testField", source="Input")
2424
assert field_ref.source == FieldSource.INPUT
2525

2626
# Create FieldReference with capitalized "Output" - should normalize to FieldSource.OUTPUT
27-
field_ref = FieldReference(path="testField", source="Output") # type: ignore[arg-type]
27+
field_ref = FieldReference(path="testField", source="Output")
2828
assert field_ref.source == FieldSource.OUTPUT
2929

3030
# Create FieldReference with lowercase "input" - should work as-is
31-
field_ref = FieldReference(path="testField", source="input") # type: ignore[arg-type]
31+
field_ref = FieldReference(path="testField", source="input")
3232
assert field_ref.source == FieldSource.INPUT
3333

3434
def test_all_fields_selector_normalizes_capitalized_sources(
@@ -37,17 +37,16 @@ def test_all_fields_selector_normalizes_capitalized_sources(
3737
"""Test that AllFieldsSelector normalizes capitalized source values in the list."""
3838
# Create AllFieldsSelector with capitalized "Input" and "Output" - should normalize
3939
selector = AllFieldsSelector(
40-
selector_type="all",
41-
sources=["Input", "Output"], # type: ignore[list-item]
40+
selector_type="all", sources=["Input", "Output"]
4241
)
4342
assert FieldSource.INPUT in selector.sources
4443
assert FieldSource.OUTPUT in selector.sources
4544
assert len(selector.sources) == 2
4645

4746
# Create AllFieldsSelector with mixed case - should normalize all
4847
selector = AllFieldsSelector(
49-
selector_type="all",
50-
sources=["Input", "output"], # type: ignore[list-item]
48+
selector_type="all", sources=["Input", "output"]
5149
)
5250
assert FieldSource.INPUT in selector.sources
5351
assert FieldSource.OUTPUT in selector.sources
52+

0 commit comments

Comments
 (0)