|
13 | 13 | UiPathConversationEvent, |
14 | 14 | UiPathConversationExchangeEndEvent, |
15 | 15 | UiPathConversationExchangeEvent, |
| 16 | + UiPathConversationExecutingToolCallEvent, |
16 | 17 | UiPathConversationMessageEvent, |
17 | 18 | UiPathConversationToolCallConfirmationEvent, |
| 19 | + UiPathConversationToolCallEndEvent, |
| 20 | + UiPathConversationToolCallEvent, |
18 | 21 | ) |
19 | 22 | from uipath.core.triggers import UiPathResumeTrigger |
20 | 23 | from uipath.runtime.chat import UiPathChatProtocol |
@@ -122,9 +125,11 @@ def __init__( |
122 | 125 | self._client: Any | None = None |
123 | 126 | self._connected_event = asyncio.Event() |
124 | 127 |
|
125 | | - self._tool_confirmation_event = asyncio.Event() |
126 | | - self._tool_confirmation_value: ( |
127 | | - UiPathConversationToolCallConfirmationEvent | None |
| 128 | + self._tool_resume_event = asyncio.Event() |
| 129 | + self._tool_resume_value: ( |
| 130 | + UiPathConversationToolCallConfirmationEvent |
| 131 | + | UiPathConversationToolCallEndEvent |
| 132 | + | None |
128 | 133 | ) = None |
129 | 134 | self._current_message_id: str | None = None |
130 | 135 |
|
@@ -362,33 +367,52 @@ async def emit_exchange_error_event(self, error: Exception) -> None: |
362 | 367 | async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger): |
363 | 368 | """No-op. |
364 | 369 |
|
365 | | - Tool confirmation — the only interrupt pattern CAS uses today — is |
366 | | - handled end-to-end via ``startToolCall`` with ``requireConfirmation: |
367 | | - true`` paired with ``wait_for_resume()``. This is deliberately |
368 | | - simpler than the old interrupt-based flow: CAS needs |
369 | | - ``requireConfirmation`` on the tool call event itself to render the |
370 | | - confirmation UI, so a parallel ``startInterrupt`` event would be |
371 | | - redundant. |
372 | | -
|
373 | | - The only hypothetical reason to put work here is a generic, |
374 | | - non-tool-call agent interrupt (e.g. a coded agent calling |
375 | | - ``interrupt("do you want to continue?")``). Nothing uses that today |
376 | | - and it's not a near-term requirement — the method is kept for |
377 | | - generic flexibility. |
| 370 | + Tool confirmation is handled end-to-end via ``startToolCall`` with |
| 371 | + ``requireConfirmation: true`` paired with ``wait_for_resume()``. |
| 372 | + executingToolCall is emitted by the MessageMapper (non-confirmed |
| 373 | + tools) and the runtime loop post-confirmation (confirmed tools). |
378 | 374 | """ |
379 | 375 | return None |
380 | 376 |
|
| 377 | + async def emit_executing_tool_call_event( |
| 378 | + self, |
| 379 | + tool_call_id: str, |
| 380 | + tool_input: dict[str, Any] | None = None, |
| 381 | + ) -> None: |
| 382 | + """Emit an executingToolCall event. |
| 383 | +
|
| 384 | + Called by the runtime loop after a tool-call confirmation resumes |
| 385 | + to signal that the tool is about to execute with the final input. |
| 386 | + """ |
| 387 | + if not self._current_message_id: |
| 388 | + return |
| 389 | + |
| 390 | + executing_event = UiPathConversationMessageEvent( |
| 391 | + message_id=self._current_message_id, |
| 392 | + tool_call=UiPathConversationToolCallEvent( |
| 393 | + tool_call_id=tool_call_id, |
| 394 | + executing=UiPathConversationExecutingToolCallEvent( |
| 395 | + input=tool_input, |
| 396 | + ), |
| 397 | + ), |
| 398 | + ) |
| 399 | + await self.emit_message_event(executing_event) |
| 400 | + |
381 | 401 | async def wait_for_resume(self) -> dict[str, Any]: |
382 | | - """Wait for a confirmToolCall event to be received.""" |
383 | | - self._tool_confirmation_event.clear() |
384 | | - self._tool_confirmation_value = None |
| 402 | + """Wait for a tool resume event (confirmToolCall or endToolCall) to be received.""" |
| 403 | + if self._tool_resume_value is None: |
| 404 | + self._tool_resume_event.clear() |
| 405 | + await self._tool_resume_event.wait() |
385 | 406 |
|
386 | | - await self._tool_confirmation_event.wait() |
| 407 | + value = self._tool_resume_value |
| 408 | + self._tool_resume_value = None |
| 409 | + self._tool_resume_event.clear() |
387 | 410 |
|
388 | | - if self._tool_confirmation_value: |
389 | | - return self._tool_confirmation_value.model_dump( |
390 | | - mode="python", by_alias=False |
391 | | - ) |
| 411 | + """For the case where there's no tool confirmation and the client side tool sends endToolCall back before wait_for_resume is called. |
| 412 | + Unlikely in practice, but possible in theory, since executingToolCall is emitted during the streaming. |
| 413 | + """ |
| 414 | + if value: |
| 415 | + return value.model_dump(mode="python", by_alias=False) |
392 | 416 | return {} |
393 | 417 |
|
394 | 418 | @property |
@@ -424,13 +448,13 @@ async def _handle_conversation_event( |
424 | 448 | parsed_event.exchange |
425 | 449 | and parsed_event.exchange.message |
426 | 450 | and (tool_call := parsed_event.exchange.message.tool_call) |
427 | | - and (confirm := tool_call.confirm) |
428 | 451 | ): |
429 | | - logger.info( |
430 | | - f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}" |
431 | | - ) |
432 | | - self._tool_confirmation_value = confirm |
433 | | - self._tool_confirmation_event.set() |
| 452 | + if confirm := tool_call.confirm: |
| 453 | + self._tool_resume_value = confirm |
| 454 | + self._tool_resume_event.set() |
| 455 | + elif end := tool_call.end: |
| 456 | + self._tool_resume_value = end |
| 457 | + self._tool_resume_event.set() |
434 | 458 | except Exception as e: |
435 | 459 | logger.warning(f"Error parsing conversation event: {e}") |
436 | 460 |
|
|
0 commit comments