diff --git a/pyproject.toml b/pyproject.toml index 5e6256b..abbf109 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.4" +version = "0.5.5" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/core/guardrails/_evaluators.py b/src/uipath/core/guardrails/_evaluators.py index 502ff7c..ff935fc 100644 --- a/src/uipath/core/guardrails/_evaluators.py +++ b/src/uipath/core/guardrails/_evaluators.py @@ -195,6 +195,9 @@ def evaluate_word_rule( ) -> tuple[bool, str]: """Evaluate a word rule against input and output data.""" fields = get_fields_from_selector(rule.field_selector, input_data, output_data) + if not fields: + return True, "No fields to validate" + operator = _humanize_guardrail_func(rule.detects_violation) or "violation check" field_paths = ", ".join({field_ref.path for _, field_ref in fields}) @@ -237,6 +240,9 @@ def evaluate_number_rule( ) -> tuple[bool, str]: """Evaluate a number rule against input and output data.""" fields = get_fields_from_selector(rule.field_selector, input_data, output_data) + if not fields: + return True, "No fields to validate" + operator = _humanize_guardrail_func(rule.detects_violation) or "violation check" field_paths = ", ".join({field_ref.path for _, field_ref in fields}) for field_value, field_ref in fields: @@ -281,6 +287,9 @@ def evaluate_boolean_rule( ) -> tuple[bool, str]: """Evaluate a boolean rule against input and output data.""" fields = get_fields_from_selector(rule.field_selector, input_data, output_data) + if not fields: + return True, "No fields to validate" + operator = _humanize_guardrail_func(rule.detects_violation) or "violation check" field_paths = ", ".join({field_ref.path for _, field_ref in fields}) for field_value, field_ref in fields: diff --git a/tests/guardrails/test_deterministic_guardrails_service.py b/tests/guardrails/test_deterministic_guardrails_service.py index 1db2da4..0bafde9 100644 --- a/tests/guardrails/test_deterministic_guardrails_service.py +++ b/tests/guardrails/test_deterministic_guardrails_service.py @@ -1466,3 +1466,104 @@ def _create_guardrail_with_always_rule( ), ], ) + + +class TestMissingFieldPassesValidation: + """Test that rules referencing missing fields pass validation.""" + + def test_word_rule_missing_field_passes( + self, service: DeterministicGuardrailsService + ) -> None: + guardrail = DeterministicGuardrail( + id="test-missing-field", + name="Missing Field Guardrail", + description="Test missing field", + enabled_for_evals=True, + guardrail_type="custom", + selector=GuardrailSelector( + scopes=[GuardrailScope.TOOL], match_names=["test"] + ), + rules=[ + WordRule( + rule_type="word", + field_selector=SpecificFieldsSelector( + selector_type="specific", + fields=[ + FieldReference(path="sentence2", source=FieldSource.INPUT) + ], + ), + detects_violation=lambda s: len(s or "") > 0, + rule_description="sentence2 is not empty", + ), + ], + ) + result = service._evaluate_deterministic_guardrail( + input_data={"sentence1": "hello"}, + output_data={}, + guardrail=guardrail, + ) + assert result.result == GuardrailValidationResultType.PASSED + + def test_number_rule_missing_field_passes( + self, service: DeterministicGuardrailsService + ) -> None: + guardrail = DeterministicGuardrail( + id="test-missing-field", + name="Missing Field Guardrail", + description="Test missing field", + enabled_for_evals=True, + guardrail_type="custom", + selector=GuardrailSelector( + scopes=[GuardrailScope.TOOL], match_names=["test"] + ), + rules=[ + NumberRule( + rule_type="number", + field_selector=SpecificFieldsSelector( + selector_type="specific", + fields=[FieldReference(path="age", source=FieldSource.INPUT)], + ), + detects_violation=lambda n: n is not None and n < 0, + rule_description="age is negative", + ), + ], + ) + result = service._evaluate_deterministic_guardrail( + input_data={"name": "test"}, + output_data={}, + guardrail=guardrail, + ) + assert result.result == GuardrailValidationResultType.PASSED + + def test_boolean_rule_missing_field_passes( + self, service: DeterministicGuardrailsService + ) -> None: + guardrail = DeterministicGuardrail( + id="test-missing-field", + name="Missing Field Guardrail", + description="Test missing field", + enabled_for_evals=True, + guardrail_type="custom", + selector=GuardrailSelector( + scopes=[GuardrailScope.TOOL], match_names=["test"] + ), + rules=[ + BooleanRule( + rule_type="boolean", + field_selector=SpecificFieldsSelector( + selector_type="specific", + fields=[ + FieldReference(path="is_active", source=FieldSource.INPUT) + ], + ), + detects_violation=lambda b: b is False, + rule_description="is_active is false", + ), + ], + ) + result = service._evaluate_deterministic_guardrail( + input_data={"name": "test"}, + output_data={}, + guardrail=guardrail, + ) + assert result.result == GuardrailValidationResultType.PASSED diff --git a/uv.lock b/uv.lock index 0e8083a..ca9bac5 100644 --- a/uv.lock +++ b/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.4" +version = "0.5.5" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" },