diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 306e9d9c6..f0f120ffa 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.5" +version = "0.5.6" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py b/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py index 502ff7cde..ff935fcd1 100644 --- a/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py +++ b/packages/uipath-core/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/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py b/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py index 1db2da454..0bafde94f 100644 --- a/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py +++ b/packages/uipath-core/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/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index ca9bac5fb..8c7f71815 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.5" +version = "0.5.6" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" },