From 14397e88614b003273cc263dd933996b9d840a56 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 27 Jan 2026 16:41:36 -0800 Subject: [PATCH 1/3] feat: inject reviewed data into response for observability Add _inject_reviewed_data() helper to pass ReviewedInputs/ReviewedOutputs from HITL escalation result through inner_state for observability callback. Co-Authored-By: Claude Opus 4.5 --- .../guardrails/actions/escalate_action.py | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py index 304330ee..a1a71d83 100644 --- a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py +++ b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py @@ -2,6 +2,7 @@ import ast import json +import logging import re from typing import Any, Dict, Literal, cast @@ -30,6 +31,8 @@ from ..utils import _extract_tool_args_from_message, get_message_content from .base_action import GuardrailAction, GuardrailActionNode +logger = logging.getLogger(__name__) + class EscalateAction(GuardrailAction): """Node-producing action that inserts a HITL interruption node into the graph. @@ -151,7 +154,17 @@ async def _node( ) if escalation_result.action == "Approve": - return _process_escalation_response( + # Extract reviewed data for observability + reviewed_inputs = escalation_result.data.get("ReviewedInputs") + reviewed_outputs = escalation_result.data.get("ReviewedOutputs") + reviewed_by = escalation_result.data.get("ReviewedBy") + + logger.info( + f"HITL escalation approved: guardrail={guardrail.name}, " + f"reviewed_inputs={reviewed_inputs}, reviewed_outputs={reviewed_outputs}" + ) + + response = _process_escalation_response( state, escalation_result.data, scope, @@ -159,15 +172,72 @@ async def _node( guarded_component_name, ) + # Inject reviewed data into response for observability callback + return _inject_reviewed_data( + response, + reviewed_inputs, + reviewed_outputs, + reviewed_by, + ) + raise AgentTerminationException( code=UiPathErrorCode.EXECUTION_ERROR, title="Escalation rejected", detail=f"Please contact your administrator. Action was rejected after reviewing the task created by guardrail [{guardrail.name}], with reason: {escalation_result.data['Reason']}", ) + # Attach observability metadata to the node function + _node.__metadata__ = { # type: ignore[attr-defined] + "guardrail": guardrail, + "scope": scope, + "execution_stage": execution_stage, + "action_type": "escalate", + } + return node_name, _node +ESCALATION_REVIEWED_DATA_KEY = "_escalation_reviewed_data" + + +def _inject_reviewed_data( + response: Dict[str, Any] | Command[Any], + reviewed_inputs: Any, + reviewed_outputs: Any, + reviewed_by: Any, +) -> Dict[str, Any] | Command[Any]: + """Inject reviewed data into response for observability callback to read. + + Args: + response: The response from _process_escalation_response. + reviewed_inputs: The reviewed inputs from escalation result. + reviewed_outputs: The reviewed outputs from escalation result. + reviewed_by: The reviewer from escalation result. + + Returns: + The response with reviewed data injected into inner_state. + """ + reviewed_data = { + "reviewed_inputs": reviewed_inputs, + "reviewed_outputs": reviewed_outputs, + "reviewed_by": reviewed_by, + } + + if isinstance(response, Command): + update = dict(response.update) if response.update else {} + inner_state = dict(update.get("inner_state", {})) + inner_state[ESCALATION_REVIEWED_DATA_KEY] = reviewed_data + update["inner_state"] = inner_state + return Command(update=update, graph=response.graph) + elif isinstance(response, dict): + inner_state = dict(response.get("inner_state", {})) + inner_state[ESCALATION_REVIEWED_DATA_KEY] = reviewed_data + response["inner_state"] = inner_state + return response + + return response + + def _validate_message_count( state: AgentGuardrailsGraphState, execution_stage: ExecutionStage, From 29b236c59d4a1eac5ec32323788bece3d587e3ce Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 27 Jan 2026 16:48:13 -0800 Subject: [PATCH 2/3] refactor: remove unused metadata and logging from escalate_action Co-Authored-By: Claude Opus 4.5 --- .../agent/guardrails/actions/escalate_action.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py index fbc4b0d1..f95fa09b 100644 --- a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py +++ b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py @@ -2,7 +2,6 @@ import ast import json -import logging import re from typing import Any, Dict, Literal, cast @@ -31,8 +30,6 @@ from ..utils import _extract_tool_args_from_message, get_message_content from .base_action import GuardrailAction, GuardrailActionNode -logger = logging.getLogger(__name__) - class EscalateAction(GuardrailAction): """Node-producing action that inserts a HITL interruption node into the graph. @@ -159,11 +156,6 @@ async def _node( reviewed_outputs = escalation_result.data.get("ReviewedOutputs") reviewed_by = escalation_result.data.get("ReviewedBy") - logger.info( - f"HITL escalation approved: guardrail={guardrail.name}, " - f"reviewed_inputs={reviewed_inputs}, reviewed_outputs={reviewed_outputs}" - ) - response = _process_escalation_response( state, escalation_result.data, @@ -186,14 +178,6 @@ async def _node( detail=f"Please contact your administrator. Action was rejected after reviewing the task created by guardrail [{guardrail.name}], with reason: {escalation_result.data['Reason']}", ) - # Attach observability metadata to the node function - _node.__metadata__ = { # type: ignore[attr-defined] - "guardrail": guardrail, - "scope": scope, - "execution_stage": execution_stage, - "action_type": "Escalate", - } - return node_name, _node From c631b3c5f08f53c3806918703436be84b8647663 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Tue, 27 Jan 2026 19:21:39 -0800 Subject: [PATCH 3/3] feat: add metadata to escalate action for observability Add __metadata__ with action_type to escalate action node, consistent with other action nodes (block, log, filter). This enables callback to detect action nodes via metadata instead of relying on node name suffix patterns. Co-Authored-By: Claude Opus 4.5 --- .../agent/guardrails/actions/escalate_action.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py index f95fa09b..0dcbdb31 100644 --- a/src/uipath_langchain/agent/guardrails/actions/escalate_action.py +++ b/src/uipath_langchain/agent/guardrails/actions/escalate_action.py @@ -178,6 +178,13 @@ async def _node( detail=f"Please contact your administrator. Action was rejected after reviewing the task created by guardrail [{guardrail.name}], with reason: {escalation_result.data['Reason']}", ) + _node.__metadata__ = { # type: ignore[attr-defined] + "guardrail": guardrail, + "scope": scope, + "execution_stage": execution_stage, + "action_type": "Escalate", + } + return node_name, _node