Skip to content

Commit adf7ceb

Browse files
Merge pull request #312 from UiPath/AL-228-log-block-guardrail-agent
feat(api): add support for OOTB guardrails at Agent level [AL-228, AL-241]
2 parents e505af5 + 79069d8 commit adf7ceb

23 files changed

+1085
-359
lines changed

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-langchain"
3-
version = "0.1.29"
3+
version = "0.1.30"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
1-
from .guardrail_nodes import (
2-
create_agent_guardrail_node,
3-
create_llm_guardrail_node,
4-
create_tool_guardrail_node,
5-
)
61
from .guardrails_factory import build_guardrails_with_actions
7-
from .guardrails_subgraph import (
8-
create_agent_guardrails_subgraph,
9-
create_llm_guardrails_subgraph,
10-
create_tool_guardrails_subgraph,
11-
)
122

133
__all__ = [
14-
"create_llm_guardrails_subgraph",
15-
"create_agent_guardrails_subgraph",
16-
"create_tool_guardrails_subgraph",
17-
"create_llm_guardrail_node",
18-
"create_agent_guardrail_node",
19-
"create_tool_guardrail_node",
204
"build_guardrails_with_actions",
215
]

src/uipath_langchain/agent/guardrails/actions/block_action.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from uipath_langchain.agent.guardrails.types import ExecutionStage
77

88
from ...exceptions import AgentTerminationException
9-
from ..types import AgentGuardrailsGraphState
9+
from ...react.types import AgentGuardrailsGraphState
1010
from .base_action import GuardrailAction, GuardrailActionNode
1111

1212

src/uipath_langchain/agent/guardrails/actions/escalate_action.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
from uipath.runtime.errors import UiPathErrorCode
1515

1616
from ...exceptions import AgentTerminationException
17-
from ..guardrail_nodes import _message_text
18-
from ..types import AgentGuardrailsGraphState, ExecutionStage
17+
from ...react.types import AgentGuardrailsGraphState
18+
from ..types import ExecutionStage
19+
from ..utils import get_message_content
1920
from .base_action import GuardrailAction, GuardrailActionNode
2021

2122

@@ -229,7 +230,7 @@ def _process_llm_escalation_response(
229230
if content_list:
230231
last_message.content = content_list[-1]
231232

232-
return Command[Any](update={"messages": msgs})
233+
return Command(update={"messages": msgs})
233234
except Exception as e:
234235
raise AgentTerminationException(
235236
code=UiPathErrorCode.EXECUTION_ERROR,
@@ -311,7 +312,7 @@ def _process_tool_escalation_response(
311312
if reviewed_outputs_json:
312313
last_message.content = reviewed_outputs_json
313314

314-
return Command[Any](update={"messages": msgs})
315+
return Command(update={"messages": msgs})
315316
except Exception as e:
316317
raise AgentTerminationException(
317318
code=UiPathErrorCode.EXECUTION_ERROR,
@@ -377,7 +378,7 @@ def _extract_llm_escalation_content(
377378
if isinstance(last_message, ToolMessage):
378379
return last_message.content
379380

380-
content = _message_text(last_message)
381+
content = get_message_content(last_message)
381382
return json.dumps(content) if content else ""
382383

383384
# For AI messages, process tool calls if present
@@ -395,14 +396,14 @@ def _extract_llm_escalation_content(
395396
):
396397
content_list.append(json.dumps(args["content"]))
397398

398-
message_content = _message_text(last_message)
399+
message_content = get_message_content(last_message)
399400
if message_content:
400401
content_list.append(message_content)
401402

402403
return json.dumps(content_list)
403404

404405
# Fallback for other message types
405-
return _message_text(last_message)
406+
return get_message_content(last_message)
406407

407408

408409
def _extract_agent_escalation_content(

src/uipath_langchain/agent/guardrails/actions/log_action.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from uipath_langchain.agent.guardrails.types import ExecutionStage
88

9-
from ..types import AgentGuardrailsGraphState
9+
from ...react.types import AgentGuardrailsGraphState
1010
from .base_action import GuardrailAction, GuardrailActionNode
1111

1212
logger = logging.getLogger(__name__)

src/uipath_langchain/agent/guardrails/guardrail_nodes.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
from typing import Any, Callable
55

6-
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, SystemMessage
6+
from langchain_core.messages import AIMessage
77
from langgraph.types import Command
88
from uipath.platform import UiPath
99
from uipath.platform.guardrails import (
@@ -12,18 +12,12 @@
1212
)
1313

1414
from uipath_langchain.agent.guardrails.types import ExecutionStage
15-
16-
from .types import AgentGuardrailsGraphState
15+
from uipath_langchain.agent.guardrails.utils import get_message_content
16+
from uipath_langchain.agent.react.types import AgentGuardrailsGraphState
1717

1818
logger = logging.getLogger(__name__)
1919

2020

21-
def _message_text(msg: AnyMessage) -> str:
22-
if isinstance(msg, (HumanMessage, SystemMessage)):
23-
return msg.content if isinstance(msg.content, str) else str(msg.content)
24-
return str(getattr(msg, "content", "")) if hasattr(msg, "content") else ""
25-
26-
2721
def _create_guardrail_node(
2822
guardrail: BaseGuardrail,
2923
scope: GuardrailScope,
@@ -70,7 +64,7 @@ def create_llm_guardrail_node(
7064
def _payload_generator(state: AgentGuardrailsGraphState) -> str:
7165
if not state.messages:
7266
return ""
73-
return _message_text(state.messages[-1])
67+
return get_message_content(state.messages[-1])
7468

7569
return _create_guardrail_node(
7670
guardrail,
@@ -82,17 +76,35 @@ def _payload_generator(state: AgentGuardrailsGraphState) -> str:
8276
)
8377

8478

85-
def create_agent_guardrail_node(
79+
def create_agent_init_guardrail_node(
8680
guardrail: BaseGuardrail,
8781
execution_stage: ExecutionStage,
8882
success_node: str,
8983
failure_node: str,
9084
) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]:
91-
# To be implemented in future PR
9285
def _payload_generator(state: AgentGuardrailsGraphState) -> str:
9386
if not state.messages:
9487
return ""
95-
return _message_text(state.messages[-1])
88+
return get_message_content(state.messages[-1])
89+
90+
return _create_guardrail_node(
91+
guardrail,
92+
GuardrailScope.AGENT,
93+
execution_stage,
94+
_payload_generator,
95+
success_node,
96+
failure_node,
97+
)
98+
99+
100+
def create_agent_terminate_guardrail_node(
101+
guardrail: BaseGuardrail,
102+
execution_stage: ExecutionStage,
103+
success_node: str,
104+
failure_node: str,
105+
) -> tuple[str, Callable[[AgentGuardrailsGraphState], Any]]:
106+
def _payload_generator(state: AgentGuardrailsGraphState) -> str:
107+
return str(state.agent_result)
96108

97109
return _create_guardrail_node(
98110
guardrail,
@@ -161,7 +173,7 @@ def _payload_generator(state: AgentGuardrailsGraphState) -> str:
161173
if args is not None:
162174
return json.dumps(args)
163175

164-
return _message_text(state.messages[-1])
176+
return get_message_content(state.messages[-1])
165177

166178
return _create_guardrail_node(
167179
guardrail,

src/uipath_langchain/agent/guardrails/types.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,4 @@
11
from enum import Enum
2-
from typing import Annotated, Optional
3-
4-
from langchain_core.messages import AnyMessage
5-
from langgraph.graph.message import add_messages
6-
from pydantic import BaseModel
7-
8-
9-
class AgentGuardrailsGraphState(BaseModel):
10-
"""Agent Guardrails Graph state for guardrail subgraph."""
11-
12-
messages: Annotated[list[AnyMessage], add_messages] = []
13-
guardrail_validation_result: Optional[str] = None
142

153

164
class ExecutionStage(str, Enum):
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
2+
3+
4+
def get_message_content(msg: AnyMessage) -> str:
5+
if isinstance(msg, (HumanMessage, SystemMessage)):
6+
return msg.content if isinstance(msg.content, str) else str(msg.content)
7+
return str(getattr(msg, "content", "")) if hasattr(msg, "content") else ""

src/uipath_langchain/agent/react/agent.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
from pydantic import BaseModel
1010
from uipath.platform.guardrails import BaseGuardrail
1111

12-
from ..guardrails import create_llm_guardrails_subgraph
1312
from ..guardrails.actions import GuardrailAction
14-
from ..guardrails.guardrails_subgraph import create_tools_guardrails_subgraph
1513
from ..tools import create_tool_node
14+
from .guardrails.guardrails_subgraph import (
15+
create_agent_init_guardrails_subgraph,
16+
create_agent_terminate_guardrails_subgraph,
17+
create_llm_guardrails_subgraph,
18+
create_tools_guardrails_subgraph,
19+
)
1620
from .init_node import (
1721
create_init_node,
1822
)
@@ -86,12 +90,20 @@ def create_agent(
8690
builder: StateGraph[AgentGraphState, None, InputT, OutputT] = StateGraph(
8791
InnerAgentGraphState, input_schema=input_schema, output_schema=output_schema
8892
)
89-
builder.add_node(AgentGraphNode.INIT, init_node)
93+
init_with_guardrails_subgraph = create_agent_init_guardrails_subgraph(
94+
(AgentGraphNode.GUARDED_INIT, init_node),
95+
guardrails,
96+
)
97+
builder.add_node(AgentGraphNode.INIT, init_with_guardrails_subgraph)
9098

9199
for tool_name, tool_node in tool_nodes_with_guardrails.items():
92100
builder.add_node(tool_name, tool_node)
93101

94-
builder.add_node(AgentGraphNode.TERMINATE, terminate_node)
102+
terminate_with_guardrails_subgraph = create_agent_terminate_guardrails_subgraph(
103+
(AgentGraphNode.GUARDED_TERMINATE, terminate_node),
104+
guardrails,
105+
)
106+
builder.add_node(AgentGraphNode.TERMINATE, terminate_with_guardrails_subgraph)
95107

96108
builder.add_edge(START, AgentGraphNode.INIT)
97109

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

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@
1010
GuardrailScope,
1111
)
1212

13-
from uipath_langchain.agent.guardrails.types import ExecutionStage
14-
15-
from .actions.base_action import GuardrailAction, GuardrailActionNode
16-
from .guardrail_nodes import (
17-
create_agent_guardrail_node,
13+
from uipath_langchain.agent.guardrails.actions.base_action import (
14+
GuardrailAction,
15+
GuardrailActionNode,
16+
)
17+
from uipath_langchain.agent.guardrails.guardrail_nodes import (
18+
create_agent_init_guardrail_node,
19+
create_agent_terminate_guardrail_node,
1820
create_llm_guardrail_node,
1921
create_tool_guardrail_node,
2022
)
21-
from .types import AgentGuardrailsGraphState
23+
from uipath_langchain.agent.guardrails.types import ExecutionStage
24+
from uipath_langchain.agent.react.types import (
25+
AgentGraphState,
26+
AgentGuardrailsGraphState,
27+
)
2228

2329
_VALIDATOR_ALLOWED_STAGES = {
2430
"prompt_injection": {ExecutionStage.PRE_EXECUTION},
@@ -232,32 +238,65 @@ def create_tools_guardrails_subgraph(
232238
return result
233239

234240

235-
def create_agent_guardrails_subgraph(
236-
agent_node: tuple[str, Any],
241+
def create_agent_init_guardrails_subgraph(
242+
init_node: tuple[str, Any],
237243
guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
238-
execution_stage: ExecutionStage,
239244
):
240-
"""Create a subgraph for AGENT-scoped guardrails that applies checks at the specified stage.
241-
242-
This is intended for wrapping nodes like INIT or TERMINATE, where guardrails should run
243-
either before (pre-execution) or after (post-execution) the node logic.
244-
"""
245+
"""Create a subgraph for INIT node that applies guardrails on the state messages."""
245246
applicable_guardrails = [
246247
(guardrail, _)
247248
for (guardrail, _) in (guardrails or [])
248249
if GuardrailScope.AGENT in guardrail.selector.scopes
249250
]
250251
if applicable_guardrails is None or len(applicable_guardrails) == 0:
251-
return agent_node[1]
252+
return init_node[1]
252253

253254
return _create_guardrails_subgraph(
254-
main_inner_node=agent_node,
255+
main_inner_node=init_node,
256+
guardrails=applicable_guardrails,
257+
scope=GuardrailScope.AGENT,
258+
execution_stages=[ExecutionStage.POST_EXECUTION],
259+
node_factory=create_agent_init_guardrail_node,
260+
)
261+
262+
263+
def create_agent_terminate_guardrails_subgraph(
264+
terminate_node: tuple[str, Any],
265+
guardrails: Sequence[tuple[BaseGuardrail, GuardrailAction]] | None,
266+
):
267+
"""Create a subgraph for TERMINATE node that applies guardrails on the agent result."""
268+
node_name, node_func = terminate_node
269+
270+
def terminate_wrapper(state: Any) -> dict[str, Any]:
271+
# Call original terminate node
272+
result = node_func(state)
273+
# Store result in state
274+
return {"agent_result": result, "messages": state.messages}
275+
276+
applicable_guardrails = [
277+
(guardrail, _)
278+
for (guardrail, _) in (guardrails or [])
279+
if GuardrailScope.AGENT in guardrail.selector.scopes
280+
]
281+
if applicable_guardrails is None or len(applicable_guardrails) == 0:
282+
return terminate_node[1]
283+
284+
subgraph = _create_guardrails_subgraph(
285+
main_inner_node=(node_name, terminate_wrapper),
255286
guardrails=applicable_guardrails,
256287
scope=GuardrailScope.AGENT,
257-
execution_stages=[execution_stage],
258-
node_factory=create_agent_guardrail_node,
288+
execution_stages=[ExecutionStage.POST_EXECUTION],
289+
node_factory=create_agent_terminate_guardrail_node,
259290
)
260291

292+
async def run_terminate_subgraph(
293+
state: AgentGraphState,
294+
) -> dict[str, Any]:
295+
result_state = await subgraph.ainvoke(state)
296+
return result_state["agent_result"]
297+
298+
return run_terminate_subgraph
299+
261300

262301
def create_tool_guardrails_subgraph(
263302
tool_node: tuple[str, Any],

0 commit comments

Comments
 (0)