Skip to content

Commit b31d876

Browse files
feat: update logic for deterministic rules for nested fields (#1524)
1 parent 488dcbf commit b31d876

6 files changed

Lines changed: 132 additions & 5 deletions

File tree

packages/uipath-core/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.8"
3+
version = "0.5.9"
44
description = "UiPath Core abstractions"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-core/src/uipath/core/guardrails/_evaluators.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ def _traverse(current: Any, remaining_parts: list[str]) -> None:
6868
field_name, array_depth = _parse_path_segment(part)
6969

7070
if isinstance(current, dict):
71+
if not field_name and array_depth != ArrayDepth.NONE:
72+
# Path segment is purely array notation (e.g. [*]) with no
73+
# field name. This happens when the root data is a
74+
# dict-wrapped array (e.g. {"output": [...]}) and the
75+
# original path starts with [*]. Iterate over all list
76+
# values in the dict so the array elements are reached.
77+
for value in current.values():
78+
if isinstance(value, list):
79+
if array_depth == ArrayDepth.MATRIX:
80+
for row in value:
81+
if isinstance(row, list):
82+
for item in row:
83+
_traverse(item, next_parts)
84+
else:
85+
_traverse(row, next_parts)
86+
else: # SINGLE
87+
for item in value:
88+
_traverse(item, next_parts)
89+
return
90+
7191
if field_name not in current:
7292
return
7393
next_value = current.get(field_name)

packages/uipath-core/tests/guardrails/test_deterministic_guardrails_service.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,3 +1567,110 @@ def test_boolean_rule_missing_field_passes(
15671567
guardrail=guardrail,
15681568
)
15691569
assert result.result == GuardrailValidationResultType.PASSED
1570+
1571+
1572+
class TestWrappedArrayOutputEvaluation:
1573+
"""Test that guardrails work when tool output is a dict-wrapped array.
1574+
1575+
When a tool returns a JSON array, _extract_tool_output_data wraps it as
1576+
{"output": [...]}. Field paths starting with [*] (e.g. [*].author.email)
1577+
must still resolve correctly against the wrapped structure.
1578+
"""
1579+
1580+
def test_word_rule_on_wrapped_array_output_detects_violation(
1581+
self, service: DeterministicGuardrailsService
1582+
) -> None:
1583+
guardrail = DeterministicGuardrail(
1584+
id="test-wrapped-array",
1585+
name="Wrapped Array Guardrail",
1586+
description="Test wrapped array output",
1587+
enabled_for_evals=True,
1588+
guardrail_type="custom",
1589+
selector=GuardrailSelector(
1590+
scopes=[GuardrailScope.TOOL], match_names=["test"]
1591+
),
1592+
rules=[
1593+
WordRule(
1594+
rule_type="word",
1595+
field_selector=SpecificFieldsSelector(
1596+
selector_type="specific",
1597+
fields=[
1598+
FieldReference(
1599+
path="[*].author.emailAddress",
1600+
source=FieldSource.OUTPUT,
1601+
)
1602+
],
1603+
),
1604+
detects_violation=lambda s: "iana" in s,
1605+
rule_description="email contains 'iana'",
1606+
),
1607+
],
1608+
)
1609+
# Simulates output from _extract_tool_output_data when tool returns an array.
1610+
# All emails contain "iana" so every field violates the rule → guardrail fails.
1611+
output_data = {
1612+
"output": [
1613+
{
1614+
"author": {"emailAddress": "briana.smith@test.com"},
1615+
"body": "comment1",
1616+
},
1617+
{
1618+
"author": {"emailAddress": "adriana.jones@test.com"},
1619+
"body": "comment2",
1620+
},
1621+
]
1622+
}
1623+
result = service.evaluate_post_deterministic_guardrail(
1624+
input_data={},
1625+
output_data=output_data,
1626+
guardrail=guardrail,
1627+
)
1628+
assert result.result == GuardrailValidationResultType.VALIDATION_FAILED
1629+
1630+
def test_word_rule_on_wrapped_array_output_passes_when_no_match(
1631+
self, service: DeterministicGuardrailsService
1632+
) -> None:
1633+
guardrail = DeterministicGuardrail(
1634+
id="test-wrapped-array",
1635+
name="Wrapped Array Guardrail",
1636+
description="Test wrapped array output",
1637+
enabled_for_evals=True,
1638+
guardrail_type="custom",
1639+
selector=GuardrailSelector(
1640+
scopes=[GuardrailScope.TOOL], match_names=["test"]
1641+
),
1642+
rules=[
1643+
WordRule(
1644+
rule_type="word",
1645+
field_selector=SpecificFieldsSelector(
1646+
selector_type="specific",
1647+
fields=[
1648+
FieldReference(
1649+
path="[*].author.emailAddress",
1650+
source=FieldSource.OUTPUT,
1651+
)
1652+
],
1653+
),
1654+
detects_violation=lambda s: "iana" in s,
1655+
rule_description="email contains 'iana'",
1656+
),
1657+
],
1658+
)
1659+
output_data = {
1660+
"output": [
1661+
{
1662+
"author": {"emailAddress": "mike.wilson@test.com"},
1663+
"body": "comment1",
1664+
},
1665+
{
1666+
"author": {"emailAddress": "sarah.clark@test.com"},
1667+
"body": "comment2",
1668+
},
1669+
]
1670+
}
1671+
result = service.evaluate_post_deterministic_guardrail(
1672+
input_data={},
1673+
output_data=output_data,
1674+
guardrail=guardrail,
1675+
)
1676+
assert result.result == GuardrailValidationResultType.PASSED

packages/uipath-core/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath-platform/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)