Skip to content

Commit e8ba143

Browse files
Merge pull request #351 from UiPath/filter-agent-llm
feat: don't allow filter action for agent and llm scopes [AL-249]
2 parents 1632842 + 9dce255 commit e8ba143

3 files changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from .base_action import GuardrailAction
22
from .block_action import BlockAction
33
from .escalate_action import EscalateAction
4+
from .filter_action import FilterAction
45
from .log_action import LogAction
56

67
__all__ = [
78
"GuardrailAction",
89
"BlockAction",
910
"LogAction",
1011
"EscalateAction",
12+
"FilterAction",
1113
]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import re
2+
from typing import Any
3+
4+
from uipath.platform.guardrails import BaseGuardrail, GuardrailScope
5+
from uipath.runtime.errors import UiPathErrorCategory, UiPathErrorCode
6+
7+
from uipath_langchain.agent.guardrails.types import ExecutionStage
8+
9+
from ...exceptions import AgentTerminationException
10+
from ...react.types import AgentGuardrailsGraphState
11+
from .base_action import GuardrailAction, GuardrailActionNode
12+
13+
14+
class FilterAction(GuardrailAction):
15+
"""Action that filters inputs/outputs on guardrail failure.
16+
17+
For now, filtering is only supported for non-AGENT and non-LLM scopes.
18+
If invoked for ``GuardrailScope.AGENT`` or ``GuardrailScope.LLM``, this action
19+
raises an exception to indicate the operation is not supported yet.
20+
"""
21+
22+
def action_node(
23+
self,
24+
*,
25+
guardrail: BaseGuardrail,
26+
scope: GuardrailScope,
27+
execution_stage: ExecutionStage,
28+
guarded_component_name: str,
29+
) -> GuardrailActionNode:
30+
"""Create a guardrail action node that performs filtering.
31+
32+
Args:
33+
guardrail: The guardrail responsible for the validation.
34+
scope: The scope in which the guardrail applies.
35+
execution_stage: Whether this runs before or after execution.
36+
guarded_component_name: Name of the guarded component.
37+
38+
Returns:
39+
A tuple containing the node name and the async node callable.
40+
"""
41+
raw_node_name = f"{scope.name}_{execution_stage.name}_{guardrail.name}_filter"
42+
node_name = re.sub(r"\W+", "_", raw_node_name.lower()).strip("_")
43+
44+
async def _node(_state: AgentGuardrailsGraphState) -> dict[str, Any]:
45+
if scope in (GuardrailScope.AGENT, GuardrailScope.LLM):
46+
raise AgentTerminationException(
47+
code=UiPathErrorCode.EXECUTION_ERROR,
48+
title="Guardrail filter action not supported",
49+
detail=f"FilterAction is not supported for scope [{scope.name}] at this time.",
50+
category=UiPathErrorCategory.USER,
51+
)
52+
# No-op for other scopes for now.
53+
return {}
54+
55+
return node_name, _node
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Tests for FilterAction guardrail failure behavior."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
from unittest.mock import MagicMock
7+
8+
import pytest
9+
from uipath.platform.guardrails import GuardrailScope
10+
from uipath.runtime.errors import UiPathErrorCode
11+
12+
from uipath_langchain.agent.exceptions import AgentTerminationException
13+
from uipath_langchain.agent.guardrails.actions.filter_action import FilterAction
14+
from uipath_langchain.agent.guardrails.types import ExecutionStage
15+
from uipath_langchain.agent.react.types import AgentGuardrailsGraphState
16+
17+
if TYPE_CHECKING:
18+
from _pytest.capture import CaptureFixture # noqa: F401
19+
from _pytest.fixtures import FixtureRequest # noqa: F401
20+
from _pytest.logging import LogCaptureFixture # noqa: F401
21+
from _pytest.monkeypatch import MonkeyPatch # noqa: F401
22+
from pytest_mock.plugin import MockerFixture # noqa: F401
23+
24+
25+
class TestFilterAction:
26+
@pytest.mark.asyncio
27+
@pytest.mark.parametrize(
28+
("scope", "stage", "expected_node_name"),
29+
[
30+
(
31+
GuardrailScope.LLM,
32+
ExecutionStage.PRE_EXECUTION,
33+
"llm_pre_execution_my_guardrail_v1_filter",
34+
),
35+
(
36+
GuardrailScope.LLM,
37+
ExecutionStage.POST_EXECUTION,
38+
"llm_post_execution_my_guardrail_v1_filter",
39+
),
40+
(
41+
GuardrailScope.AGENT,
42+
ExecutionStage.PRE_EXECUTION,
43+
"agent_pre_execution_my_guardrail_v1_filter",
44+
),
45+
(
46+
GuardrailScope.AGENT,
47+
ExecutionStage.POST_EXECUTION,
48+
"agent_post_execution_my_guardrail_v1_filter",
49+
),
50+
],
51+
)
52+
async def test_node_name_and_exception_for_unsupported_scopes(
53+
self, scope: GuardrailScope, stage: ExecutionStage, expected_node_name: str
54+
) -> None:
55+
"""AGENT/LLM scopes raise AgentTerminationException and node name is sanitized."""
56+
action = FilterAction()
57+
guardrail = MagicMock()
58+
guardrail.name = "My Guardrail v1"
59+
60+
node_name, node = action.action_node(
61+
guardrail=guardrail,
62+
scope=scope,
63+
execution_stage=stage,
64+
guarded_component_name="guarded_node_name",
65+
)
66+
67+
assert node_name == expected_node_name
68+
69+
with pytest.raises(AgentTerminationException) as excinfo:
70+
await node(AgentGuardrailsGraphState(messages=[]))
71+
72+
# Validate rich error info
73+
assert (
74+
excinfo.value.error_info.code
75+
== f"Python.{UiPathErrorCode.EXECUTION_ERROR.value}"
76+
)
77+
assert excinfo.value.error_info.title == "Guardrail filter action not supported"
78+
assert (
79+
excinfo.value.error_info.detail
80+
== f"FilterAction is not supported for scope [{scope.name}] at this time."
81+
)
82+
83+
@pytest.mark.asyncio
84+
@pytest.mark.parametrize(
85+
"stage",
86+
[ExecutionStage.PRE_EXECUTION, ExecutionStage.POST_EXECUTION],
87+
)
88+
async def test_tool_scope_returns_empty_dict(self, stage: ExecutionStage) -> None:
89+
"""TOOL scope currently performs no-op and returns empty dict."""
90+
action = FilterAction()
91+
guardrail = MagicMock()
92+
guardrail.name = "My Guardrail v1"
93+
94+
_, node = action.action_node(
95+
guardrail=guardrail,
96+
scope=GuardrailScope.TOOL,
97+
execution_stage=stage,
98+
guarded_component_name="test_tool",
99+
)
100+
101+
result = await node(AgentGuardrailsGraphState(messages=[]))
102+
assert result == {}

0 commit comments

Comments
 (0)