Skip to content

Commit ecf73a7

Browse files
author
Valentina Bojan
committed
fix: add guardrail rule_description
1 parent cfd3cdb commit ecf73a7

5 files changed

Lines changed: 196 additions & 9 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Python SDK that enables developers to build and deploy LangGraph
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
8-
"uipath>=2.5.15,<2.6.0",
8+
"uipath>=2.5.17,<2.6.0",
99
"uipath-runtime>=0.5.1,<0.6.0",
1010
"langgraph>=1.0.0, <2.0.0",
1111
"langchain-core>=1.2.5, <2.0.0",

src/uipath_langchain/agent/guardrails/guardrail_nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def _create_validation_command(
108108
if guardrail_result.result == GuardrailValidationResultType.PASSED:
109109
return Command(
110110
goto=success_node,
111-
update={"inner_state": {"guardrail_validation_result": None}},
111+
update={"inner_state": {"guardrail_validation_result": guardrail_result.reason}},
112112
)
113113

114114
if guardrail_result.result == GuardrailValidationResultType.VALIDATION_FAILED:

src/uipath_langchain/agent/guardrails/guardrails_factory.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,72 @@ def _create_word_rule_func(
155155
raise ValueError(f"Unsupported word operator: {operator}")
156156

157157

158+
def _build_field_selector_description(field_selector: AgentFieldSelector) -> str:
159+
"""Build a human-readable selector description for field selector.
160+
161+
Args:
162+
field_selector: The field selector describing which fields this rule applies to.
163+
164+
Returns:
165+
A string describing the selector, using:
166+
- \"All\" for `AgentAllFieldsSelector`
167+
- Comma-separated field paths for `SpecificFieldsSelector`
168+
- ``str(field_selector)`` as a fallback.
169+
"""
170+
if isinstance(field_selector, AgentAllFieldsSelector):
171+
return "All fields"
172+
if isinstance(field_selector, SpecificFieldsSelector):
173+
field_paths = [field.path for field in field_selector.fields]
174+
return ", ".join(field_paths)
175+
return str(field_selector)
176+
177+
178+
def _build_rule_description(
179+
operator: AgentWordOperator | AgentNumberOperator | AgentBooleanOperator,
180+
value: str | float | bool | None,
181+
field_selector: AgentFieldSelector,
182+
) -> str:
183+
"""Build the full human-readable description for a word rule.
184+
185+
Args:
186+
operator: The word operator to describe.
187+
value: The comparison value, if applicable for the operator.
188+
field_selector: The field selector describing which fields this rule applies to.
189+
190+
Returns:
191+
A string describing the rule, combining selector, operator, and value.
192+
"""
193+
selector_description = _build_field_selector_description(field_selector)
194+
195+
if operator in {
196+
AgentWordOperator.CONTAINS,
197+
AgentWordOperator.DOES_NOT_CONTAIN,
198+
AgentWordOperator.EQUALS,
199+
AgentWordOperator.DOES_NOT_EQUAL,
200+
AgentWordOperator.STARTS_WITH,
201+
AgentWordOperator.DOES_NOT_START_WITH,
202+
AgentWordOperator.ENDS_WITH,
203+
AgentWordOperator.DOES_NOT_END_WITH,
204+
AgentWordOperator.MATCHES_REGEX,
205+
AgentNumberOperator.EQUALS,
206+
AgentNumberOperator.DOES_NOT_EQUAL,
207+
AgentNumberOperator.GREATER_THAN,
208+
AgentNumberOperator.GREATER_THAN_OR_EQUAL,
209+
AgentNumberOperator.LESS_THAN,
210+
AgentNumberOperator.LESS_THAN_OR_EQUAL,
211+
AgentBooleanOperator.EQUALS,
212+
}:
213+
return f"{selector_description} {operator.value} {value!r}"
214+
215+
if operator in {
216+
AgentWordOperator.IS_EMPTY,
217+
AgentWordOperator.IS_NOT_EMPTY,
218+
}:
219+
return f"{selector_description} {operator.value}"
220+
221+
raise ValueError(f"Unsupported word operator: {operator}")
222+
223+
158224
def _create_number_rule_func(
159225
operator: AgentNumberOperator, value: float
160226
) -> Callable[[float], bool]:
@@ -314,6 +380,9 @@ def _convert_agent_rule_to_deterministic(
314380
detects_violation=_create_word_rule_func(
315381
agent_rule.operator, agent_rule.value
316382
),
383+
rule_description=_build_rule_description(
384+
agent_rule.operator, agent_rule.value, agent_rule.field_selector
385+
),
317386
)
318387

319388
if isinstance(agent_rule, AgentNumberRule):
@@ -325,6 +394,9 @@ def _convert_agent_rule_to_deterministic(
325394
detects_violation=_create_number_rule_func(
326395
agent_rule.operator, agent_rule.value
327396
),
397+
rule_description=_build_rule_description(
398+
agent_rule.operator, agent_rule.value, agent_rule.field_selector
399+
),
328400
)
329401

330402
if isinstance(agent_rule, AgentBooleanRule):
@@ -336,6 +408,9 @@ def _convert_agent_rule_to_deterministic(
336408
detects_violation=_create_boolean_rule_func(
337409
agent_rule.operator, agent_rule.value
338410
),
411+
rule_description=_build_rule_description(
412+
agent_rule.operator, agent_rule.value, agent_rule.field_selector
413+
),
339414
)
340415

341416
raise ValueError(f"Unsupported agent rule type: {type(agent_rule)}")

tests/agent/guardrails/test_guardrails_factory.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from uipath_langchain.agent.guardrails.actions.filter_action import FilterAction
4545
from uipath_langchain.agent.guardrails.actions.log_action import LogAction
4646
from uipath_langchain.agent.guardrails.guardrails_factory import (
47+
_build_rule_description,
4748
_convert_agent_custom_guardrail_to_deterministic,
4849
_convert_agent_rule_to_deterministic,
4950
_create_boolean_rule_func,
@@ -562,6 +563,117 @@ def test_unsupported_operator_raises_value_error(self) -> None:
562563
_create_boolean_rule_func(cast(AgentBooleanOperator, "INVALID"), True)
563564

564565

566+
class TestBuildRuleDescription:
567+
"""Tests for _build_rule_description."""
568+
569+
def test_word_rule_with_specific_field_selector(self) -> None:
570+
"""Description for word rule with specific selector includes field path, operator, and value."""
571+
word_rule = AgentWordRule.model_validate(
572+
{
573+
"$ruleType": "word",
574+
"fieldSelector": {
575+
"$selectorType": "specific",
576+
"fields": [{"path": "message.content", "source": "input"}],
577+
},
578+
"operator": "contains",
579+
"value": "forbidden",
580+
}
581+
)
582+
583+
description = _build_rule_description(
584+
word_rule.operator,
585+
word_rule.value,
586+
word_rule.field_selector,
587+
)
588+
589+
assert description == "message.content contains 'forbidden'"
590+
591+
def test_word_rule_with_all_field_selector(self) -> None:
592+
"""Description for word rule with all selector uses 'All' as selector description."""
593+
word_rule = AgentWordRule.model_validate(
594+
{
595+
"$ruleType": "word",
596+
"fieldSelector": {"$selectorType": "all"},
597+
"operator": "equals",
598+
"value": "test",
599+
}
600+
)
601+
602+
description = _build_rule_description(
603+
word_rule.operator,
604+
word_rule.value,
605+
word_rule.field_selector,
606+
)
607+
608+
assert description == "All fields equals 'test'"
609+
610+
def test_word_rule_without_value_operator(self) -> None:
611+
"""Description for word rule without value omits the value portion."""
612+
word_rule = AgentWordRule.model_validate(
613+
{
614+
"$ruleType": "word",
615+
"fieldSelector": {
616+
"$selectorType": "specific",
617+
"fields": [{"path": "message.content", "source": "input"}],
618+
},
619+
"operator": "isEmpty",
620+
"value": None,
621+
}
622+
)
623+
624+
description = _build_rule_description(
625+
word_rule.operator,
626+
word_rule.value,
627+
word_rule.field_selector,
628+
)
629+
630+
assert description == "message.content isEmpty"
631+
632+
def test_number_rule_description(self) -> None:
633+
"""Description for number rule includes numeric comparison operator and value."""
634+
number_rule = AgentNumberRule.model_validate(
635+
{
636+
"$ruleType": "number",
637+
"fieldSelector": {
638+
"$selectorType": "specific",
639+
"fields": [{"path": "data.count", "source": "input"}],
640+
},
641+
"operator": "greaterThan",
642+
"value": 10.0,
643+
}
644+
)
645+
646+
description = _build_rule_description(
647+
number_rule.operator,
648+
number_rule.value,
649+
number_rule.field_selector,
650+
)
651+
652+
assert description == "data.count greaterThan 10.0"
653+
654+
def test_boolean_rule_description(self) -> None:
655+
"""Description for boolean rule includes field path, equals operator, and boolean value."""
656+
boolean_rule = AgentBooleanRule.model_validate(
657+
{
658+
"$ruleType": "boolean",
659+
"fieldSelector": {
660+
"$selectorType": "specific",
661+
"fields": [{"path": "data.is_active", "source": "input"}],
662+
},
663+
"operator": "equals",
664+
"value": True,
665+
}
666+
)
667+
668+
description = _build_rule_description(
669+
boolean_rule.operator,
670+
boolean_rule.value,
671+
boolean_rule.field_selector,
672+
)
673+
674+
assert description == "data.is_active equals True"
675+
676+
565677
class TestConvertAgentRuleToDeterministic:
566678
"""Tests for _convert_agent_rule_to_deterministic."""
567679

uv.lock

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

0 commit comments

Comments
 (0)