Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
20 changes: 20 additions & 0 deletions packages/uipath-core/src/uipath/core/guardrails/_evaluators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion packages/uipath-core/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading