From 0708fd2a4296ed13326c6ef41424bf497ce75758 Mon Sep 17 00:00:00 2001 From: "diana.pirvulescu" Date: Fri, 27 Mar 2026 17:51:06 +0200 Subject: [PATCH 1/6] feat: update logic for deterministic rules for nested fields --- .../src/uipath/core/guardrails/_evaluators.py | 20 ++++ .../test_deterministic_guardrails_service.py | 95 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py b/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py index ff935fcd1..51c90413c 100644 --- a/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py +++ b/packages/uipath-core/src/uipath/core/guardrails/_evaluators.py @@ -68,6 +68,26 @@ def _traverse(current: Any, remaining_parts: list[str]) -> None: field_name, array_depth = _parse_path_segment(part) if isinstance(current, dict): + if not field_name and array_depth != ArrayDepth.NONE: + # Path segment is purely array notation (e.g. [*]) with no + # field name. This happens when the root data is a + # dict-wrapped array (e.g. {"output": [...]}) and the + # original path starts with [*]. Iterate over all list + # values in the dict so the array elements are reached. + for value in current.values(): + if isinstance(value, list): + if array_depth == ArrayDepth.MATRIX: + for row in value: + if isinstance(row, list): + for item in row: + _traverse(item, next_parts) + else: + _traverse(row, next_parts) + else: # SINGLE + for item in value: + _traverse(item, next_parts) + return + if field_name not in current: return next_value = current.get(field_name) 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 0bafde94f..76f0d6233 100644 --- a/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py +++ b/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py @@ -1567,3 +1567,98 @@ def test_boolean_rule_missing_field_passes( guardrail=guardrail, ) assert result.result == GuardrailValidationResultType.PASSED + + +class TestWrappedArrayOutputEvaluation: + """Test that guardrails work when tool output is a dict-wrapped array. + + When a tool returns a JSON array, _extract_tool_output_data wraps it as + {"output": [...]}. Field paths starting with [*] (e.g. [*].author.email) + must still resolve correctly against the wrapped structure. + """ + + def test_word_rule_on_wrapped_array_output_detects_violation( + self, service: DeterministicGuardrailsService + ) -> None: + guardrail = DeterministicGuardrail( + id="test-wrapped-array", + name="Wrapped Array Guardrail", + description="Test wrapped array output", + 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="[*].author.emailAddress", + source=FieldSource.OUTPUT, + ) + ], + ), + detects_violation=lambda s: "iana" in s, + rule_description="email contains 'iana'", + ), + ], + ) + # Simulates output from _extract_tool_output_data when tool returns an array. + # All emails contain "iana" so every field violates the rule → guardrail fails. + output_data = { + "output": [ + {"author": {"emailAddress": "briana.smith@test.com"}, "body": "comment1"}, + {"author": {"emailAddress": "adriana.jones@test.com"}, "body": "comment2"}, + ] + } + result = service.evaluate_post_deterministic_guardrail( + input_data={}, + output_data=output_data, + guardrail=guardrail, + ) + assert result.result == GuardrailValidationResultType.VALIDATION_FAILED + + def test_word_rule_on_wrapped_array_output_passes_when_no_match( + self, service: DeterministicGuardrailsService + ) -> None: + guardrail = DeterministicGuardrail( + id="test-wrapped-array", + name="Wrapped Array Guardrail", + description="Test wrapped array output", + 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="[*].author.emailAddress", + source=FieldSource.OUTPUT, + ) + ], + ), + detects_violation=lambda s: "iana" in s, + rule_description="email contains 'iana'", + ), + ], + ) + output_data = { + "output": [ + {"author": {"emailAddress": "mike.wilson@test.com"}, "body": "comment1"}, + {"author": {"emailAddress": "sarah.clark@test.com"}, "body": "comment2"}, + ] + } + result = service.evaluate_post_deterministic_guardrail( + input_data={}, + output_data=output_data, + guardrail=guardrail, + ) + assert result.result == GuardrailValidationResultType.PASSED From f46d0a0c49c0d83410bdc3ef478d909d9c3f997b Mon Sep 17 00:00:00 2001 From: "diana.pirvulescu" Date: Fri, 27 Mar 2026 18:12:12 +0200 Subject: [PATCH 2/6] fix: update version --- packages/uipath-core/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index b495babf6..99b1a6f09 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.8" +version = "0.5.9" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" From 675a9fea99820a0232f68d2eee8da53153f2b476 Mon Sep 17 00:00:00 2001 From: "diana.pirvulescu" Date: Fri, 27 Mar 2026 18:17:49 +0200 Subject: [PATCH 3/6] fix: update version --- packages/uipath-core/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index f90fa8d2e..97bfa5ff1 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.8" +version = "0.5.9" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, From 02c31f2aa3a018019e6ef408eb6281fb1ebee45f Mon Sep 17 00:00:00 2001 From: "diana.pirvulescu" Date: Fri, 27 Mar 2026 18:24:01 +0200 Subject: [PATCH 4/6] fix: update version --- packages/uipath-platform/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index c405963e9..f1031f586 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.8" +version = "0.5.9" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 46be9a394ca005fc5c5d45244ac20c1d0da8d2a4 Mon Sep 17 00:00:00 2001 From: "diana.pirvulescu" Date: Mon, 30 Mar 2026 11:16:51 +0300 Subject: [PATCH 5/6] fix: fix lint --- .../test_deterministic_guardrails_service.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 76f0d6233..e506f49ac 100644 --- a/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py +++ b/packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py @@ -1610,8 +1610,14 @@ def test_word_rule_on_wrapped_array_output_detects_violation( # All emails contain "iana" so every field violates the rule → guardrail fails. output_data = { "output": [ - {"author": {"emailAddress": "briana.smith@test.com"}, "body": "comment1"}, - {"author": {"emailAddress": "adriana.jones@test.com"}, "body": "comment2"}, + { + "author": {"emailAddress": "briana.smith@test.com"}, + "body": "comment1", + }, + { + "author": {"emailAddress": "adriana.jones@test.com"}, + "body": "comment2", + }, ] } result = service.evaluate_post_deterministic_guardrail( @@ -1652,8 +1658,14 @@ def test_word_rule_on_wrapped_array_output_passes_when_no_match( ) output_data = { "output": [ - {"author": {"emailAddress": "mike.wilson@test.com"}, "body": "comment1"}, - {"author": {"emailAddress": "sarah.clark@test.com"}, "body": "comment2"}, + { + "author": {"emailAddress": "mike.wilson@test.com"}, + "body": "comment1", + }, + { + "author": {"emailAddress": "sarah.clark@test.com"}, + "body": "comment2", + }, ] } result = service.evaluate_post_deterministic_guardrail( From 9b20c712d98a0554315231856cd24c3636afc0ff Mon Sep 17 00:00:00 2001 From: "diana.pirvulescu" Date: Mon, 30 Mar 2026 11:20:58 +0300 Subject: [PATCH 6/6] fix: uv lock --- packages/uipath/uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 252a1c998..07104daff 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" [[package]] @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.8" +version = "0.5.9" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" },