@@ -90,15 +90,20 @@ async def _node(
9090 # PRE_EXECUTION: Only Inputs field from last message
9191 input_content = _extract_escalation_content (
9292 state .messages [- 1 ],
93+ state ,
9394 scope ,
9495 execution_stage ,
9596 guarded_component_name ,
9697 )
9798 data ["Inputs" ] = input_content
9899 else : # POST_EXECUTION
99- # Extract Inputs from second-to-last message using PRE_EXECUTION logic
100+ if scope == GuardrailScope .AGENT :
101+ input_message = state .messages [1 ]
102+ else :
103+ input_message = state .messages [- 2 ]
100104 input_content = _extract_escalation_content (
101- state .messages [- 2 ],
105+ input_message ,
106+ state ,
102107 scope ,
103108 ExecutionStage .PRE_EXECUTION ,
104109 guarded_component_name ,
@@ -107,6 +112,7 @@ async def _node(
107112 # Extract Outputs from last message using POST_EXECUTION logic
108113 output_content = _extract_escalation_content (
109114 state .messages [- 1 ],
115+ state ,
110116 scope ,
111117 execution_stage ,
112118 guarded_component_name ,
@@ -119,7 +125,7 @@ async def _node(
119125 CreateEscalation (
120126 app_name = self .app_name ,
121127 app_folder_path = self .app_folder_path ,
122- title = self . app_name ,
128+ title = "Agents Guardrail Task" ,
123129 data = data ,
124130 assignee = self .assignee ,
125131 )
@@ -137,7 +143,7 @@ async def _node(
137143 raise AgentTerminationException (
138144 code = UiPathErrorCode .EXECUTION_ERROR ,
139145 title = "Escalation rejected" ,
140- detail = f"Action was rejected after reviewing the task created by guardrail [{ guardrail .name } ]. Please contact your administrator. " ,
146+ detail = f"Please contact your administrator. Action was rejected after reviewing the task created by guardrail [{ guardrail .name } ], with reason: { escalation_result . data [ 'Reason' ] } " ,
141147 )
142148
143149 return node_name , _node
@@ -179,8 +185,8 @@ def _validate_message_count(
179185def _get_node_name (
180186 execution_stage : ExecutionStage , guardrail : BaseGuardrail , scope : GuardrailScope
181187) -> str :
182- sanitized = re . sub ( r"\W+" , "_" , guardrail .name ). strip ( "_" ). lower ()
183- node_name = f" { sanitized } _hitl_ { execution_stage . name . lower () } _ { scope .lower ()} "
188+ raw_node_name = f" { scope .name } _ { execution_stage . name } _ { guardrail . name } _hitl"
189+ node_name = re . sub ( r"\W+" , "_" , raw_node_name .lower ()). strip ( "_" )
184190 return node_name
185191
186192
@@ -200,8 +206,8 @@ def _process_escalation_response(
200206 execution_stage: The execution stage (PRE_EXECUTION or POST_EXECUTION).
201207
202208 Returns:
203- For LLM/TOOL scope: Command to update messages with reviewed inputs/outputs, or empty dict.
204- For AGENT scope: Empty dict ( no message alteration) .
209+ Command updates for the state (e.g., updating messages / tool calls / agent_result),
210+ or an empty dict if no update is needed .
205211 """
206212 match scope :
207213 case GuardrailScope .LLM :
@@ -213,8 +219,71 @@ def _process_escalation_response(
213219 state , escalation_result , execution_stage , guarded_node_name
214220 )
215221 case GuardrailScope .AGENT :
222+ return _process_agent_escalation_response (
223+ state , escalation_result , execution_stage
224+ )
225+
226+
227+ def _process_agent_escalation_response (
228+ state : AgentGuardrailsGraphState ,
229+ escalation_result : Dict [str , Any ],
230+ execution_stage : ExecutionStage ,
231+ ) -> Dict [str , Any ] | Command [Any ]:
232+ """Process escalation response for AGENT scope guardrails.
233+
234+ For AGENT scope:
235+ - PRE_EXECUTION: updates the last message content using `ReviewedInputs`
236+ - POST_EXECUTION: updates `agent_result` using `ReviewedOutputs`
237+
238+ Args:
239+ state: The current agent graph state.
240+ escalation_result: The result from the escalation interrupt containing reviewed inputs/outputs.
241+ execution_stage: The execution stage (PRE_EXECUTION or POST_EXECUTION).
242+
243+ Returns:
244+ Command to update state, or empty dict if no updates are needed.
245+
246+ Raises:
247+ AgentTerminationException: If escalation response processing fails.
248+ """
249+ try :
250+ reviewed_field = get_reviewed_field_name (execution_stage )
251+ if reviewed_field not in escalation_result :
252+ return {}
253+
254+ reviewed_value = escalation_result .get (reviewed_field )
255+ if not reviewed_value :
216256 return {}
217257
258+ try :
259+ parsed = json .loads (reviewed_value )
260+ except json .JSONDecodeError :
261+ parsed = reviewed_value
262+
263+ if execution_stage == ExecutionStage .PRE_EXECUTION :
264+ msgs = state .messages .copy ()
265+ if not msgs :
266+ return {}
267+ msgs [- 1 ].content = parsed
268+ return Command (update = {"messages" : msgs })
269+
270+ # POST_EXECUTION: update agent_result
271+ return Command (update = {"agent_result" : parsed })
272+ except Exception as e :
273+ raise AgentTerminationException (
274+ code = UiPathErrorCode .EXECUTION_ERROR ,
275+ title = "Escalation rejected" ,
276+ detail = str (e ),
277+ ) from e
278+
279+
280+ def get_reviewed_field_name (execution_stage ):
281+ return (
282+ "ReviewedInputs"
283+ if execution_stage == ExecutionStage .PRE_EXECUTION
284+ else "ReviewedOutputs"
285+ )
286+
218287
219288def _process_llm_escalation_response (
220289 state : AgentGuardrailsGraphState ,
@@ -237,11 +306,7 @@ def _process_llm_escalation_response(
237306 AgentTerminationException: If escalation response processing fails.
238307 """
239308 try :
240- reviewed_field = (
241- "ReviewedInputs"
242- if execution_stage == ExecutionStage .PRE_EXECUTION
243- else "ReviewedOutputs"
244- )
309+ reviewed_field = get_reviewed_field_name (execution_stage )
245310
246311 msgs = state .messages .copy ()
247312 if not msgs or reviewed_field not in escalation_result :
@@ -342,11 +407,7 @@ def _process_tool_escalation_response(
342407 AgentTerminationException: If escalation response processing fails.
343408 """
344409 try :
345- reviewed_field = (
346- "ReviewedInputs"
347- if execution_stage == ExecutionStage .PRE_EXECUTION
348- else "ReviewedOutputs"
349- )
410+ reviewed_field = get_reviewed_field_name (execution_stage )
350411
351412 msgs = state .messages .copy ()
352413 if not msgs or reviewed_field not in escalation_result :
@@ -404,6 +465,7 @@ def _process_tool_escalation_response(
404465
405466def _extract_escalation_content (
406467 message : BaseMessage ,
468+ state : AgentGuardrailsGraphState ,
407469 scope : GuardrailScope ,
408470 execution_stage : ExecutionStage ,
409471 guarded_node_name : str ,
@@ -424,13 +486,36 @@ def _extract_escalation_content(
424486 case GuardrailScope .LLM :
425487 return _extract_llm_escalation_content (message , execution_stage )
426488 case GuardrailScope .AGENT :
427- return _extract_agent_escalation_content (message , execution_stage )
489+ return _extract_agent_escalation_content (message , state , execution_stage )
428490 case GuardrailScope .TOOL :
429491 return _extract_tool_escalation_content (
430492 message , execution_stage , guarded_node_name
431493 )
432494
433495
496+ def _extract_agent_escalation_content (
497+ message : BaseMessage ,
498+ state : AgentGuardrailsGraphState ,
499+ execution_stage : ExecutionStage ,
500+ ) -> str | list [str | Dict [str , Any ]]:
501+ """Extract escalation content for AGENT scope guardrails.
502+
503+ Args:
504+ message: The message used to extract the agent input content.
505+ state: The current agent guardrails graph state. Used to read `agent_result` for POST_EXECUTION.
506+ execution_stage: PRE_EXECUTION or POST_EXECUTION.
507+
508+ Returns:
509+ - PRE_EXECUTION: the agent input string (from message content).
510+ - POST_EXECUTION: a JSON-serialized representation of `state.agent_result`.
511+ """
512+ if execution_stage == ExecutionStage .PRE_EXECUTION :
513+ return get_message_content (cast (AnyMessage , message ))
514+
515+ output_content = state .agent_result or ""
516+ return json .dumps (output_content )
517+
518+
434519def _extract_llm_escalation_content (
435520 message : BaseMessage , execution_stage : ExecutionStage
436521) -> str | list [str | Dict [str , Any ]]:
@@ -449,8 +534,7 @@ def _extract_llm_escalation_content(
449534 if isinstance (message , ToolMessage ):
450535 return message .content
451536
452- content = get_message_content (cast (AnyMessage , message ))
453- return json .dumps (content ) if content else ""
537+ return get_message_content (cast (AnyMessage , message ))
454538
455539 # For AI messages, process tool calls if present
456540 if isinstance (message , AIMessage ):
@@ -470,21 +554,6 @@ def _extract_llm_escalation_content(
470554 return get_message_content (cast (AnyMessage , message ))
471555
472556
473- def _extract_agent_escalation_content (
474- message : BaseMessage , execution_stage : ExecutionStage
475- ) -> str | list [str | Dict [str , Any ]]:
476- """Extract escalation content for AGENT scope guardrails.
477-
478- Args:
479- message: The message to extract content from.
480- execution_stage: The execution stage (PRE_EXECUTION or POST_EXECUTION).
481-
482- Returns:
483- str: Empty string (AGENT scope guardrails do not extract escalation content).
484- """
485- return ""
486-
487-
488557def _extract_tool_escalation_content (
489558 message : BaseMessage , execution_stage : ExecutionStage , tool_name : str
490559) -> str | list [str | Dict [str , Any ]]:
0 commit comments