Skip to content

Commit 0765153

Browse files
committed
except GraphBubbleUp to handle durable_interrupt
1 parent 3a029a8 commit 0765153

4 files changed

Lines changed: 28 additions & 17 deletions

File tree

src/uipath_langchain/agent/tools/tool_node.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from langchain_core.messages.tool import ToolCall, ToolMessage
88
from langchain_core.tools import BaseTool
99
from langgraph._internal._runnable import RunnableCallable
10+
from langgraph.errors import GraphBubbleUp
1011
from langgraph.types import Command
1112
from pydantic import BaseModel
1213
from uipath.platform.resume_triggers import is_no_content_marker
@@ -80,10 +81,10 @@ def _func(self, state: AgentGraphState) -> OutputType:
8081
if call is None:
8182
return None
8283

83-
# HITL: prompt user for approval if tool requires confirmation
84+
# prompt user for approval if tool requires confirmation
8485
confirmation = request_tool_confirmation(call, self.tool)
8586

86-
# HITL cancelled: user rejected the tool call
87+
# user rejected the tool call
8788
if confirmation is not None and confirmation.cancelled:
8889
return self._process_result(call, confirmation.cancelled)
8990

@@ -96,10 +97,12 @@ def _func(self, state: AgentGraphState) -> OutputType:
9697
else:
9798
result = self.tool.invoke(call)
9899
output = self._process_result(call, result)
99-
# HITL approved: tag result with approved args (and whether they were modified)
100+
# HITL approved - apply confirmation metadata to tool result message
100101
if confirmation is not None:
101102
confirmation.annotate_result(output)
102103
return output
104+
except GraphBubbleUp:
105+
raise
103106
except Exception as e:
104107
if self.handle_tool_errors:
105108
return self._process_error_result(call, e)
@@ -110,10 +113,10 @@ async def _afunc(self, state: AgentGraphState) -> OutputType:
110113
if call is None:
111114
return None
112115

113-
# HITL: prompt user for approval if tool requires confirmation
116+
# prompt user for approval if tool requires confirmation
114117
confirmation = request_tool_confirmation(call, self.tool)
115118

116-
# HITL cancelled: user rejected the tool call
119+
# user rejected the tool call
117120
if confirmation is not None and confirmation.cancelled:
118121
return self._process_result(call, confirmation.cancelled)
119122

@@ -126,10 +129,12 @@ async def _afunc(self, state: AgentGraphState) -> OutputType:
126129
else:
127130
result = await self.tool.ainvoke(call)
128131
output = self._process_result(call, result)
129-
# HITL approved: tag result with approved args (and whether they were modified)
132+
# HITL approved - apply confirmation metadata to tool result message
130133
if confirmation is not None:
131134
confirmation.annotate_result(output)
132135
return output
136+
except GraphBubbleUp:
137+
raise
133138
except Exception as e:
134139
if self.handle_tool_errors:
135140
return self._process_error_result(call, e)

src/uipath_langchain/runtime/messages.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(self, runtime_id: str, storage: UiPathRuntimeStorageProtocol | None
6060
"""Initialize the mapper with empty state."""
6161
self.runtime_id = runtime_id
6262
self.storage = storage
63-
self.confirmation_tool_names: set[str] = set()
63+
self.tool_names_requiring_confirmation: set[str] = set()
6464
self.current_message: AIMessageChunk
6565
self.seen_message_ids: set[str] = set()
6666
self._storage_lock = asyncio.Lock()
@@ -393,8 +393,11 @@ async def map_current_message_to_start_tool_call_events(self):
393393
self.current_message.id
394394
)
395395

396-
if tool_call["name"] not in self.confirmation_tool_names:
397-
# if tool requires HITL, we skip start tool call
396+
# if tool requires confirmation, we skip start tool call
397+
if (
398+
tool_call["name"]
399+
not in self.tool_names_requiring_confirmation
400+
):
398401
events.append(
399402
self.map_tool_call_to_tool_call_start_event(
400403
self.current_message.id, tool_call
@@ -434,7 +437,7 @@ async def map_tool_message_to_events(
434437

435438
events: list[UiPathConversationMessageEvent] = []
436439

437-
# Emit deferred startToolCall for confirmation tools (skipped in Pass 1)
440+
# emit startToolCall for tools requiring confirmation after it's approved
438441
approved_args = message.response_metadata.get(CONVERSATIONAL_APPROVED_TOOL_ARGS)
439442
if approved_args is not None:
440443
tool_call = ToolCall(
@@ -683,6 +686,7 @@ def _map_langchain_ai_message_to_uipath_message_data(
683686
role="assistant",
684687
content_parts=content_parts,
685688
tool_calls=uipath_tool_calls,
689+
interrupts=[],
686690
)
687691

688692

src/uipath_langchain/runtime/runtime.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def __init__(
6565
self.entrypoint: str | None = entrypoint
6666
self.callbacks: list[BaseCallbackHandler] = callbacks or []
6767
self.chat = UiPathChatMessagesMapper(self.runtime_id, storage)
68-
self.chat.confirmation_tool_names = self._get_confirmation_tool_names()
68+
self.chat.tool_names_requiring_confirmation = (
69+
self._get_tool_names_requiring_confirmation()
70+
)
6971
self._middleware_node_names: set[str] = self._detect_middleware_nodes()
7072

7173
async def execute(
@@ -488,10 +490,10 @@ def _detect_middleware_nodes(self) -> set[str]:
488490

489491
return middleware_nodes
490492

491-
def _get_confirmation_tool_names(self) -> set[str]:
493+
def _get_tool_names_requiring_confirmation(self) -> set[str]:
492494
names: set[str] = set()
493495
for node_name, node_spec in self.graph.nodes.items():
494-
# PregelNode.bound -> Runnable, Runnable.tool -> BaseTool (if tool node)
496+
# langgraph's processing node.bound -> runnable.tool -> baseTool (if tool node)
495497
tool = getattr(getattr(node_spec, "bound", None), "tool", None)
496498
if tool is None:
497499
continue

tests/runtime/test_chat_message_mapper.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,7 +1729,7 @@ async def test_start_tool_call_skipped_for_confirmation_tool(self):
17291729
storage = create_mock_storage()
17301730
storage.get_value.return_value = {}
17311731
mapper = UiPathChatMessagesMapper("test-runtime", storage)
1732-
mapper.confirmation_tool_names = {"confirm_tool"}
1732+
mapper.tool_names_requiring_confirmation = {"confirm_tool"}
17331733

17341734
# First chunk starts the message with a confirmation tool call
17351735
first_chunk = AIMessageChunk(
@@ -1758,7 +1758,7 @@ async def test_start_tool_call_emitted_for_non_confirmation_tool(self):
17581758
storage = create_mock_storage()
17591759
storage.get_value.return_value = {}
17601760
mapper = UiPathChatMessagesMapper("test-runtime", storage)
1761-
mapper.confirmation_tool_names = {"other_tool"}
1761+
mapper.tool_names_requiring_confirmation = {"other_tool"}
17621762

17631763
first_chunk = AIMessageChunk(
17641764
content="",
@@ -1790,7 +1790,7 @@ async def test_deferred_start_tool_call_emitted_from_tool_message(self):
17901790
storage = create_mock_storage()
17911791
storage.get_value.return_value = {"tc-3": "msg-3"}
17921792
mapper = UiPathChatMessagesMapper("test-runtime", storage)
1793-
mapper.confirmation_tool_names = {"confirm_tool"}
1793+
mapper.tool_names_requiring_confirmation = {"confirm_tool"}
17941794

17951795
approved_args = {"query": "approved value"}
17961796
tool_msg = ToolMessage(
@@ -1824,7 +1824,7 @@ async def test_mixed_tools_only_confirmation_deferred(self):
18241824
storage = create_mock_storage()
18251825
storage.get_value.return_value = {}
18261826
mapper = UiPathChatMessagesMapper("test-runtime", storage)
1827-
mapper.confirmation_tool_names = {"confirm_tool"}
1827+
mapper.tool_names_requiring_confirmation = {"confirm_tool"}
18281828

18291829
first_chunk = AIMessageChunk(
18301830
content="",

0 commit comments

Comments
 (0)