Skip to content

Commit 514cd2d

Browse files
authored
fix: vs escalation tool configs (#556)
1 parent b612392 commit 514cd2d

6 files changed

Lines changed: 260 additions & 139 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.5.41"
3+
version = "0.5.42"
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"
77
dependencies = [
8-
"uipath>=2.8.10,<2.9.0",
8+
"uipath>=2.8.12,<2.9.0",
99
"uipath-runtime>=0.7.1, <0.8.0",
1010
"langgraph>=1.0.0, <2.0.0",
1111
"langchain-core>=1.2.5, <2.0.0",

src/uipath_langchain/agent/tools/escalation_tool.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
AgentEscalationResourceConfig,
1616
AssetRecipient,
1717
StandardRecipient,
18-
TaskTitle,
19-
TextBuilderTaskTitle,
2018
)
21-
from uipath.agent.utils.text_tokens import build_string_from_tokens
2219
from uipath.eval.mocks import mockable
2320
from uipath.platform import UiPath
2421
from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType
@@ -36,7 +33,11 @@
3633
from ..exceptions import AgentTerminationException
3734
from ..react.types import AgentGraphState
3835
from .tool_node import ToolWrapperReturnType
39-
from .utils import sanitize_dict_for_serialization, sanitize_tool_name
36+
from .utils import (
37+
resolve_task_title,
38+
sanitize_dict_for_serialization,
39+
sanitize_tool_name,
40+
)
4041

4142

4243
class EscalationAction(str, Enum):
@@ -86,19 +87,6 @@ async def resolve_asset(asset_name: str, folder_path: str) -> str | None:
8687
) from e
8788

8889

89-
def _resolve_task_title(
90-
task_title: TaskTitle | str | None, agent_input: dict[str, Any]
91-
) -> str:
92-
"""Resolve task title based on channel configuration."""
93-
if isinstance(task_title, TextBuilderTaskTitle):
94-
return build_string_from_tokens(task_title.tokens, agent_input)
95-
96-
if isinstance(task_title, str):
97-
return task_title
98-
99-
return "Escalation Task"
100-
101-
10290
def _get_user_email(user: Any) -> str | None:
10391
"""Extract email from user object/dict."""
10492
if user is None:
@@ -253,8 +241,10 @@ async def escalation_wrapper(
253241
if tool.metadata is None:
254242
raise RuntimeError("Tool metadata is required for task_title resolution")
255243

256-
tool.metadata["task_title"] = _resolve_task_title(
257-
channel.task_title, sanitize_dict_for_serialization(dict(state))
244+
tool.metadata["task_title"] = resolve_task_title(
245+
channel.task_title,
246+
sanitize_dict_for_serialization(dict(state)),
247+
default_title="Escalation Task",
258248
)
259249

260250
tool.metadata["_call_id"] = call.get("id")

src/uipath_langchain/agent/tools/ixp_escalation_tool.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"""Ixp escalation tool."""
22

3+
from typing import Any
4+
35
from langchain.tools import BaseTool
46
from langchain_core.messages import ToolCall
57
from langchain_core.tools import StructuredTool
8+
from langgraph.func import task
69
from langgraph.types import interrupt
710
from pydantic import BaseModel
811
from uipath.agent.models.agent import AgentIxpVsEscalationResourceConfig
912
from uipath.eval.mocks import mockable
10-
from uipath.platform.common import DocumentExtractionValidation
13+
from uipath.platform import UiPath
14+
from uipath.platform.common import WaitDocumentExtractionValidation
1115
from uipath.platform.documents import (
1216
ActionPriority,
1317
ExtractionResponseIXP,
@@ -21,7 +25,11 @@
2125
)
2226

2327
from .structured_tool_with_output_type import StructuredToolWithOutputType
24-
from .utils import sanitize_tool_name
28+
from .utils import (
29+
resolve_task_title,
30+
sanitize_dict_for_serialization,
31+
sanitize_tool_name,
32+
)
2533

2634

2735
class StructuredToolWithWrapper(StructuredToolWithOutputType, ToolWrapperMixin):
@@ -31,19 +39,14 @@ class StructuredToolWithWrapper(StructuredToolWithOutputType, ToolWrapperMixin):
3139
def create_ixp_escalation_tool(
3240
resource: AgentIxpVsEscalationResourceConfig,
3341
) -> StructuredTool:
34-
"""Uses interrupt() to suspend graph execution until data is extracted (handled by runtime)."""
42+
"""Uses interrupt() to suspend graph execution until extraction is validated."""
3543
tool_name: str = sanitize_tool_name(resource.name)
3644
storage_bucket_name: str = resource.vs_escalation_properties.storage_bucket_name
3745
storage_bucket_folder_path: str = (
3846
resource.vs_escalation_properties.storage_bucket_folder_path
3947
)
40-
action_priority: ActionPriority = (
41-
resource.vs_escalation_properties.action_priority or ActionPriority.MEDIUM
42-
)
43-
action_title: str = (
44-
resource.vs_escalation_properties.action_title or "VS Escalation Task"
45-
)
46-
48+
channel = resource.channels[0]
49+
action_priority = ActionPriority.from_str(channel.priority)
4750
ixp_tool_name: str = resource.vs_escalation_properties.ixp_tool_id
4851

4952
class OutputSchema(BaseModel):
@@ -59,14 +62,26 @@ class OutputSchema(BaseModel):
5962
async def ixp_escalation_tool(
6063
extraction_result: ExtractionResponseIXP,
6164
) -> OutputSchema:
62-
response = interrupt(
63-
DocumentExtractionValidation(
65+
task_title = "VS Escalation Task"
66+
if tool.metadata is not None:
67+
task_title = tool.metadata.get("task_title") or task_title
68+
69+
@task
70+
async def start_extraction_validation() -> Any:
71+
client = UiPath()
72+
return await client.documents.start_ixp_extraction_validation_async(
6473
extraction_response=extraction_result,
65-
action_title=action_title,
74+
action_title=task_title,
6675
storage_bucket_name=storage_bucket_name,
67-
action_folder=storage_bucket_folder_path,
76+
storage_bucket_directory_path=storage_bucket_folder_path,
6877
action_priority=action_priority,
6978
)
79+
80+
validation_response = await start_extraction_validation()
81+
response = interrupt(
82+
WaitDocumentExtractionValidation(
83+
extraction_validation=validation_response,
84+
)
7085
)
7186
return OutputSchema(data=response["dataProjection"])
7287

@@ -75,6 +90,15 @@ async def ixp_escalation_tool_wrapper(
7590
call: ToolCall,
7691
state: AgentGraphState,
7792
) -> ToolWrapperReturnType:
93+
if tool.metadata is None:
94+
raise RuntimeError("Tool metadata is required for task_title resolution")
95+
96+
tool.metadata["task_title"] = resolve_task_title(
97+
channel.task_title,
98+
sanitize_dict_for_serialization(dict(state)),
99+
default_title="VS Escalation Task",
100+
)
101+
78102
extraction_result = state.inner_state.tools_storage.get(ixp_tool_name)
79103
if not extraction_result:
80104
raise RuntimeError(
@@ -90,6 +114,13 @@ async def ixp_escalation_tool_wrapper(
90114
args_schema={},
91115
coroutine=ixp_escalation_tool,
92116
output_type=OutputSchema,
117+
metadata={
118+
"tool_type": "vs_escalation",
119+
"display_name": channel.properties.app_name,
120+
"channel_type": channel.type,
121+
"ixp_tool_id": ixp_tool_name,
122+
"storage_bucket_name": storage_bucket_name,
123+
},
93124
)
94125
tool.set_tool_wrappers(awrapper=ixp_escalation_tool_wrapper)
95126

src/uipath_langchain/agent/tools/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import re
44
from typing import Any
55

6+
from uipath.agent.models.agent import TaskTitle, TextBuilderTaskTitle
7+
from uipath.agent.utils.text_tokens import build_string_from_tokens
8+
69

710
def sanitize_tool_name(name: str) -> str:
811
"""Sanitize tool name for LLM compatibility (alphanumeric, underscore, hyphen only, max 64 chars)."""
@@ -40,3 +43,18 @@ def sanitize_dict_for_serialization(args: dict[str, Any]) -> dict[str, Any]:
4043
else:
4144
converted_args[key] = value
4245
return converted_args
46+
47+
48+
def resolve_task_title(
49+
task_title: TaskTitle | str | None,
50+
agent_input: dict[str, Any],
51+
default_title: str = "Escalation Task",
52+
) -> str:
53+
"""Resolve task title based on channel configuration."""
54+
if isinstance(task_title, TextBuilderTaskTitle):
55+
return build_string_from_tokens(task_title.tokens, agent_input)
56+
57+
if isinstance(task_title, str):
58+
return task_title
59+
60+
return default_title

0 commit comments

Comments
 (0)