Skip to content

Commit ba38823

Browse files
Merge branch 'main' into fix-escalation-tool-calls-extraction
2 parents 480bd56 + b6b23c2 commit ba38823

4 files changed

Lines changed: 150 additions & 7 deletions

File tree

src/uipath_langchain/agent/guardrails/guardrail_nodes.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,22 @@ async def node(
149149
state, guardrail, payload_generator
150150
)
151151
else:
152-
raise AgentTerminationException(
153-
code=UiPathErrorCode.EXECUTION_ERROR,
154-
title="Unsupported guardrail type",
155-
detail=f"Guardrail type '{type(guardrail).__name__}' is not supported. "
156-
f"Expected DeterministicGuardrail or BuiltInValidatorGuardrail.",
157-
)
152+
# Provide specific error message for DeterministicGuardrails with wrong scope
153+
if isinstance(guardrail, DeterministicGuardrail):
154+
raise AgentTerminationException(
155+
code=UiPathErrorCode.EXECUTION_ERROR,
156+
title="Invalid guardrail scope",
157+
detail=f"DeterministicGuardrail '{guardrail.name}' can only be used with TOOL scope. "
158+
f"Current scope: {scope.name}. "
159+
f"Please configure this guardrail to use only TOOL scope.",
160+
)
161+
else:
162+
raise AgentTerminationException(
163+
code=UiPathErrorCode.EXECUTION_ERROR,
164+
title="Unsupported guardrail type",
165+
detail=f"Guardrail type '{type(guardrail).__name__}' is not supported. "
166+
f"Expected DeterministicGuardrail (TOOL scope only) or BuiltInValidatorGuardrail.",
167+
)
158168

159169
return _create_validation_command(result, success_node, failure_node)
160170

src/uipath_langchain/agent/guardrails/guardrails_factory.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
UniversalRule,
2525
WordRule,
2626
)
27-
from uipath.platform.guardrails import BaseGuardrail
27+
from uipath.platform.guardrails import BaseGuardrail, GuardrailScope
2828

2929
from uipath_langchain.agent.guardrails.actions import (
3030
BlockAction,
@@ -233,6 +233,19 @@ def build_guardrails_with_actions(
233233
converted_guardrail = _convert_agent_custom_guardrail_to_deterministic(
234234
guardrail
235235
)
236+
# Validate that DeterministicGuardrails only have TOOL scope
237+
non_tool_scopes = [
238+
scope
239+
for scope in converted_guardrail.selector.scopes
240+
if scope != GuardrailScope.TOOL
241+
]
242+
243+
if non_tool_scopes:
244+
raise ValueError(
245+
f"Deterministic guardrail '{converted_guardrail.name}' can only be used with TOOL scope. "
246+
f"Found invalid scopes: {[scope.name for scope in non_tool_scopes]}. "
247+
f"Please configure this guardrail to use only TOOL scope."
248+
)
236249

237250
action = guardrail.action
238251

src/uipath_langchain/agent/react/guardrails/guardrails_subgraph.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from langgraph._internal._runnable import RunnableCallable
55
from langgraph.constants import END, START
66
from langgraph.graph import StateGraph
7+
from uipath.core.guardrails import DeterministicGuardrail
78
from uipath.platform.guardrails import (
89
BaseGuardrail,
910
BuiltInValidatorGuardrail,
@@ -207,6 +208,7 @@ def create_llm_guardrails_subgraph(
207208
(guardrail, _)
208209
for (guardrail, _) in (guardrails or [])
209210
if GuardrailScope.LLM in guardrail.selector.scopes
211+
and not isinstance(guardrail, DeterministicGuardrail)
210212
]
211213
if applicable_guardrails is None or len(applicable_guardrails) == 0:
212214
return llm_node[1]
@@ -247,6 +249,7 @@ def create_agent_init_guardrails_subgraph(
247249
(guardrail, _)
248250
for (guardrail, _) in (guardrails or [])
249251
if GuardrailScope.AGENT in guardrail.selector.scopes
252+
and not isinstance(guardrail, DeterministicGuardrail)
250253
]
251254
if applicable_guardrails is None or len(applicable_guardrails) == 0:
252255
return init_node[1]
@@ -277,6 +280,7 @@ def terminate_wrapper(state: Any) -> dict[str, Any]:
277280
(guardrail, _)
278281
for (guardrail, _) in (guardrails or [])
279282
if GuardrailScope.AGENT in guardrail.selector.scopes
283+
and not isinstance(guardrail, DeterministicGuardrail)
280284
]
281285
if applicable_guardrails is None or len(applicable_guardrails) == 0:
282286
return terminate_node[1]

tests/agent/guardrails/test_guardrails_factory.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,122 @@ def test_escalate_action_is_mapped_with_app_and_recipient(self) -> None:
158158
assert action.version == 2
159159
assert action.assignee == "admin@example.com"
160160

161+
@pytest.mark.parametrize(
162+
"scope,scope_lower",
163+
[
164+
("Llm", "llm"),
165+
("Agent", "agent"),
166+
],
167+
)
168+
def test_deterministic_guardrail_with_invalid_scope_raises_value_error(
169+
self, scope: str, scope_lower: str
170+
) -> None:
171+
"""DeterministicGuardrails with LLM or AGENT scope should raise ValueError."""
172+
guardrail = AgentCustomGuardrail.model_validate(
173+
{
174+
"$guardrailType": "custom",
175+
"id": f"test-{scope_lower}-scope",
176+
"name": f"test-guardrail-{scope_lower}",
177+
"description": f"Test guardrail with {scope} scope",
178+
"enabledForEvals": True,
179+
"selector": {
180+
"$selectorType": "scoped",
181+
"scopes": [scope], # Invalid scope - should be rejected
182+
"matchNames": None,
183+
},
184+
"rules": [
185+
{
186+
"$ruleType": "word",
187+
"fieldSelector": {
188+
"$selectorType": "specific",
189+
"fields": [{"path": "message.content", "source": "input"}],
190+
},
191+
"operator": "contains",
192+
"value": "forbidden",
193+
}
194+
],
195+
"action": {"$actionType": "block", "reason": "test"},
196+
}
197+
)
198+
199+
with pytest.raises(
200+
ValueError,
201+
match=rf"Deterministic guardrail 'test-guardrail-{scope_lower}' can only be used with TOOL scope.*Found invalid scopes.*{scope.upper()}",
202+
):
203+
build_guardrails_with_actions([guardrail])
204+
205+
def test_deterministic_guardrail_with_tool_scope_succeeds(self) -> None:
206+
"""DeterministicGuardrails with TOOL scope should be accepted."""
207+
guardrail = AgentCustomGuardrail.model_validate(
208+
{
209+
"$guardrailType": "custom",
210+
"id": "test-tool-scope",
211+
"name": "test-guardrail-tool",
212+
"description": "Test guardrail with TOOL scope",
213+
"enabledForEvals": True,
214+
"selector": {
215+
"$selectorType": "scoped",
216+
"scopes": ["Tool"], # TOOL scope - should be accepted
217+
"matchNames": ["my_tool"],
218+
},
219+
"rules": [
220+
{
221+
"$ruleType": "word",
222+
"fieldSelector": {
223+
"$selectorType": "specific",
224+
"fields": [{"path": "message.content", "source": "input"}],
225+
},
226+
"operator": "contains",
227+
"value": "forbidden",
228+
}
229+
],
230+
"action": {"$actionType": "block", "reason": "test"},
231+
}
232+
)
233+
234+
result = build_guardrails_with_actions([guardrail])
235+
236+
assert len(result) == 1
237+
converted_guardrail, action = result[0]
238+
assert isinstance(converted_guardrail, DeterministicGuardrail)
239+
assert converted_guardrail.name == "test-guardrail-tool"
240+
assert isinstance(action, BlockAction)
241+
242+
def test_deterministic_guardrail_with_mixed_scopes_raises_value_error(self) -> None:
243+
"""DeterministicGuardrails with mixed scopes including non-TOOL should raise ValueError."""
244+
guardrail = AgentCustomGuardrail.model_validate(
245+
{
246+
"$guardrailType": "custom",
247+
"id": "test-mixed-scope",
248+
"name": "test-guardrail-mixed",
249+
"description": "Test guardrail with mixed scopes",
250+
"enabledForEvals": True,
251+
"selector": {
252+
"$selectorType": "scoped",
253+
"scopes": ["Tool", "Llm"], # Mixed scopes - should be rejected
254+
"matchNames": ["my_tool"],
255+
},
256+
"rules": [
257+
{
258+
"$ruleType": "word",
259+
"fieldSelector": {
260+
"$selectorType": "specific",
261+
"fields": [{"path": "message.content", "source": "input"}],
262+
},
263+
"operator": "contains",
264+
"value": "forbidden",
265+
}
266+
],
267+
"action": {"$actionType": "block", "reason": "test"},
268+
}
269+
)
270+
271+
with pytest.raises(
272+
ValueError,
273+
match=r"Deterministic guardrail 'test-guardrail-mixed' can only be used with TOOL scope.*Found invalid scopes.*LLM",
274+
):
275+
build_guardrails_with_actions([guardrail])
276+
161277

162278
class TestCreateWordRuleFunc:
163279
"""Tests for _create_word_rule_func."""

0 commit comments

Comments
 (0)