Skip to content

Commit d712378

Browse files
valentinabojanValentina Bojan
andauthored
feat(guardrails): enrich observability data for guardrails (#518)
Co-authored-by: Valentina Bojan <valentina.bojan@uipath.com>
1 parent adb0490 commit d712378

6 files changed

Lines changed: 41 additions & 16 deletions

File tree

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.5.23"
3+
version = "0.5.24"
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"

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def action_node(
9191
"guardrail": guardrail,
9292
"scope": scope,
9393
"execution_stage": execution_stage,
94+
"escalation_data": {},
9495
}
9596

9697
async def _node(
@@ -103,13 +104,13 @@ async def _node(
103104
task_recipient = await resolve_recipient_value(self.recipient)
104105

105106
if isinstance(self.recipient, StandardRecipient):
106-
metadata["assigned_to"] = (
107+
metadata["escalation_data"]["assigned_to"] = (
107108
self.recipient.display_name
108109
if self.recipient.display_name
109110
else self.recipient.value
110111
)
111112
elif isinstance(self.recipient, AssetRecipient):
112-
metadata["assigned_to"] = (
113+
metadata["escalation_data"]["assigned_to"] = (
113114
task_recipient.value if task_recipient else None
114115
)
115116

@@ -184,6 +185,27 @@ async def _node(
184185
)
185186
)
186187

188+
# Store reviewed inputs/outputs in metadata for observability
189+
if escalation_result.data:
190+
reviewed_inputs = escalation_result.data.get("ReviewedInputs")
191+
reviewed_outputs = escalation_result.data.get("ReviewedOutputs")
192+
reason = escalation_result.data.get("Reason")
193+
if reviewed_inputs:
194+
metadata["escalation_data"]["reviewed_inputs"] = reviewed_inputs
195+
if reviewed_outputs:
196+
metadata["escalation_data"]["reviewed_outputs"] = reviewed_outputs
197+
if reason:
198+
metadata["escalation_data"]["reason"] = reason
199+
200+
# Store reviewed_by from completed_by_user
201+
completed_by_user = getattr(escalation_result, "completed_by_user", None)
202+
if completed_by_user:
203+
reviewed_by = completed_by_user.get(
204+
"displayName"
205+
) or completed_by_user.get("emailAddress")
206+
if reviewed_by:
207+
metadata["escalation_data"]["reviewed_by"] = reviewed_by
208+
187209
if escalation_result.action == "Approve":
188210
return _process_escalation_response(
189211
state,
@@ -196,7 +218,7 @@ async def _node(
196218
raise AgentTerminationException(
197219
code=UiPathErrorCode.EXECUTION_ERROR,
198220
title="Escalation rejected",
199-
detail=f"Please contact your administrator. Action was rejected after reviewing the task created by guardrail [{guardrail.name}], with reason: {escalation_result.data['Reason']}",
221+
detail=f"Please contact your administrator. Action was rejected after reviewing the task created by guardrail [{guardrail.name}], with reason: {escalation_result.data.get('Reason', None)}",
200222
)
201223

202224
_node.__metadata__ = metadata # type: ignore[attr-defined]

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ def action_node(
7575
"guardrail": guardrail,
7676
"scope": scope,
7777
"execution_stage": execution_stage,
78-
"excluded_fields": self.fields,
78+
"excluded_fields": [field.path for field in self.fields]
79+
if self.fields
80+
else [],
81+
"updated_data": {"input": None, "output": None},
7982
}
8083

8184
async def _node(
@@ -89,8 +92,8 @@ async def _node(
8992
guarded_component_name,
9093
)
9194
# Update metadata with filter results
92-
metadata["updated_input"] = result.updated_input
93-
metadata["updated_output"] = result.updated_output
95+
metadata["updated_data"]["input"] = result.updated_input
96+
metadata["updated_data"]["output"] = result.updated_output
9497
return result.command
9598

9699
raise AgentTerminationException(

tests/agent/guardrails/actions/test_escalate_action.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,7 +1673,7 @@ async def test_standard_recipient_assigned_to_uses_value(self, mock_interrupt):
16731673

16741674
metadata = getattr(node, "__metadata__", None)
16751675
assert metadata is not None
1676-
assert metadata["assigned_to"] == "user@example.com"
1676+
assert metadata["escalation_data"]["assigned_to"] == "user@example.com"
16771677

16781678
@pytest.mark.asyncio
16791679
@patch("uipath_langchain.agent.guardrails.actions.escalate_action.interrupt")
@@ -1711,7 +1711,7 @@ async def test_standard_recipient_assigned_to_uses_display_name(
17111711

17121712
metadata = getattr(node, "__metadata__", None)
17131713
assert metadata is not None
1714-
assert metadata["assigned_to"] == "John Doe"
1714+
assert metadata["escalation_data"]["assigned_to"] == "John Doe"
17151715

17161716
@pytest.mark.asyncio
17171717
@patch("uipath_langchain.agent.tools.escalation_tool.resolve_recipient_value")
@@ -1756,4 +1756,4 @@ async def test_asset_recipient_assigned_to_uses_resolved_value(
17561756
metadata = getattr(node, "__metadata__", None)
17571757
assert metadata is not None
17581758
# AssetRecipient uses resolved task_recipient.value
1759-
assert metadata["assigned_to"] == "resolved@example.com"
1759+
assert metadata["escalation_data"]["assigned_to"] == "resolved@example.com"

tests/agent/guardrails/actions/test_filter_action.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ def test_filter_action_node_has_excluded_fields_in_metadata(self):
519519
metadata = getattr(node, "__metadata__", None)
520520
assert metadata is not None
521521
assert "excluded_fields" in metadata
522-
assert metadata["excluded_fields"] == fields
522+
assert metadata["excluded_fields"] == ["sentence"]
523523

524524
@pytest.mark.asyncio
525525
async def test_filter_input_updates_metadata_with_updated_input(self) -> None:
@@ -553,8 +553,8 @@ async def test_filter_input_updates_metadata_with_updated_input(self) -> None:
553553
metadata = getattr(node, "__metadata__", None)
554554
assert metadata is not None
555555
# updated_input should contain the filtered args (without 'sentence')
556-
assert metadata["updated_input"] == {"other_param": "value"}
557-
assert metadata["updated_output"] is None
556+
assert metadata["updated_data"]["input"] == {"other_param": "value"}
557+
assert metadata["updated_data"]["output"] is None
558558

559559
@pytest.mark.asyncio
560560
async def test_filter_output_updates_metadata_with_updated_output(self) -> None:
@@ -583,5 +583,5 @@ async def test_filter_output_updates_metadata_with_updated_output(self) -> None:
583583
metadata = getattr(node, "__metadata__", None)
584584
assert metadata is not None
585585
# updated_output should contain the filtered output (without 'content')
586-
assert metadata["updated_output"] == {"other": "data"}
587-
assert metadata["updated_input"] is None
586+
assert metadata["updated_data"]["output"] == {"other": "data"}
587+
assert metadata["updated_data"]["input"] is None

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)