Skip to content

Commit 8aeb635

Browse files
feat: resolve ArgumentEmail and ArgumentGroupName recipients at runtime (#735)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4737e48 commit 8aeb635

5 files changed

Lines changed: 52 additions & 10 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Python SDK that enables developers to build and deploy LangGraph
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
8-
"uipath>=2.10.29, <2.11.0",
8+
"uipath>=2.10.49, <2.11.0",
99
"uipath-core>=0.5.2, <0.6.0",
1010
"uipath-platform>=0.1.25, <0.2.0",
1111
"uipath-runtime>=0.10.0, <0.11.0",

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,20 @@ async def _create_task_node(
115115
return {}
116116

117117
# Lazy import to avoid circular dependency with escalation_tool
118+
from ...react.types import AgentGraphState
118119
from ...tools.escalation_tool import resolve_recipient_value
120+
from ...tools.utils import sanitize_dict_for_serialization
119121

120-
# Resolve recipient value (handles both StandardRecipient and AssetRecipient)
121-
task_recipient = await resolve_recipient_value(self.recipient)
122+
internal_fields = set(AgentGraphState.model_fields.keys())
123+
state_dict = sanitize_dict_for_serialization(dict(state))
124+
input_args = {
125+
k: v for k, v in state_dict.items() if k not in internal_fields
126+
}
127+
128+
# Resolve recipient value (handles StandardRecipient, AssetRecipient, and argument-based recipients)
129+
task_recipient = await resolve_recipient_value(
130+
self.recipient, input_args=input_args
131+
)
122132

123133
if isinstance(self.recipient, StandardRecipient):
124134
metadata["escalation_data"]["assigned_to"] = (

src/uipath_langchain/agent/tools/escalation_tool.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
AgentEscalationRecipient,
1212
AgentEscalationRecipientType,
1313
AgentEscalationResourceConfig,
14+
ArgumentEmailRecipient,
15+
ArgumentGroupNameRecipient,
1416
AssetRecipient,
1517
StandardRecipient,
1618
)
19+
from uipath.agent.utils.text_tokens import safe_get_nested
1720
from uipath.eval.mocks import mockable
1821
from uipath.platform import UiPath
1922
from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType
@@ -46,6 +49,7 @@ class EscalationAction(str, Enum):
4649

4750
async def resolve_recipient_value(
4851
recipient: AgentEscalationRecipient,
52+
input_args: dict[str, Any] | None = None,
4953
) -> TaskRecipient | None:
5054
"""Resolve recipient value based on recipient type."""
5155
if isinstance(recipient, AssetRecipient):
@@ -57,6 +61,26 @@ async def resolve_recipient_value(
5761
type = TaskRecipientType.GROUP_NAME
5862
return TaskRecipient(value=value, type=type, displayName=value)
5963

64+
if isinstance(recipient, ArgumentEmailRecipient):
65+
value = safe_get_nested(input_args or {}, recipient.argument_path)
66+
if value is None:
67+
raise ValueError(
68+
f"Argument '{recipient.argument_path}' has no value in agent input."
69+
)
70+
return TaskRecipient(
71+
value=value, type=TaskRecipientType.EMAIL, displayName=value
72+
)
73+
74+
if isinstance(recipient, ArgumentGroupNameRecipient):
75+
value = safe_get_nested(input_args or {}, recipient.argument_path)
76+
if value is None:
77+
raise ValueError(
78+
f"Argument '{recipient.argument_path}' has no value in agent input."
79+
)
80+
return TaskRecipient(
81+
value=value, type=TaskRecipientType.GROUP_NAME, displayName=value
82+
)
83+
6084
if isinstance(recipient, StandardRecipient):
6185
type = TaskRecipientType(recipient.type)
6286
if recipient.type == AgentEscalationRecipientType.USER_EMAIL:
@@ -156,8 +180,11 @@ class EscalationToolOutput(BaseModel):
156180
_bts_context: dict[str, Any] = {}
157181

158182
async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]:
183+
agent_input: dict[str, Any] = (
184+
tool.metadata.get("agent_input") if tool.metadata else None
185+
) or {}
159186
recipient: TaskRecipient | None = (
160-
await resolve_recipient_value(channel.recipients[0])
187+
await resolve_recipient_value(channel.recipients[0], input_args=agent_input)
161188
if channel.recipients
162189
else None
163190
)
@@ -249,11 +276,16 @@ async def escalation_wrapper(
249276
if tool.metadata is None:
250277
raise RuntimeError("Tool metadata is required for task_title resolution")
251278

279+
state_dict = sanitize_dict_for_serialization(dict(state))
252280
tool.metadata["task_title"] = resolve_task_title(
253281
channel.task_title,
254-
sanitize_dict_for_serialization(dict(state)),
282+
state_dict,
255283
default_title="Escalation Task",
256284
)
285+
internal_fields = set(AgentGraphState.model_fields.keys())
286+
tool.metadata["agent_input"] = {
287+
k: v for k, v in state_dict.items() if k not in internal_fields
288+
}
257289

258290
tool.metadata["_call_id"] = call.get("id")
259291
tool.metadata["_call_args"] = dict(call.get("args", {}))

tests/agent/guardrails/actions/test_escalate_action.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1763,7 +1763,7 @@ async def test_create_task_resolves_recipient_correctly(
17631763

17641764
await create_task_fn(state)
17651765

1766-
mock_resolve_recipient.assert_called_once_with(recipient)
1766+
mock_resolve_recipient.assert_called_once_with(recipient, input_args={})
17671767
call_kwargs = mock_client.tasks.create_async.call_args[1]
17681768
assert call_kwargs["recipient"] == expected_value
17691769

uv.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)