1010 UiPathConversationMessageEvent ,
1111 UiPathConversationMessageStartEvent ,
1212)
13- from uipath .core .triggers import UiPathResumeTrigger , UiPathResumeTriggerType
13+ from uipath .core .triggers import (
14+ UiPathApiTrigger ,
15+ UiPathResumeTrigger ,
16+ UiPathResumeTriggerType ,
17+ )
1418
1519from uipath .runtime import (
1620 UiPathExecuteOptions ,
2226 UiPathChatProtocol ,
2327 UiPathChatRuntime ,
2428)
29+ from uipath .runtime .chat .runtime import _parse_confirmation
2530from uipath .runtime .events import UiPathRuntimeEvent , UiPathRuntimeMessageEvent
2631from uipath .runtime .schema import UiPathRuntimeSchema
2732
@@ -34,6 +39,7 @@ def make_chat_bridge_mock() -> UiPathChatProtocol:
3439 bridge_mock .disconnect = AsyncMock ()
3540 bridge_mock .emit_message_event = AsyncMock ()
3641 bridge_mock .emit_interrupt_event = AsyncMock ()
42+ bridge_mock .emit_executing_tool_call_event = AsyncMock ()
3743 bridge_mock .wait_for_resume = AsyncMock ()
3844
3945 return cast (UiPathChatProtocol , bridge_mock )
@@ -144,6 +150,13 @@ async def stream(
144150 trigger = UiPathResumeTrigger (
145151 interrupt_id = "interrupt-1" ,
146152 trigger_type = UiPathResumeTriggerType .API ,
153+ api_resume = UiPathApiTrigger (
154+ request = {
155+ "tool_call_id" : "tc-1" ,
156+ "tool_name" : "test_tool" ,
157+ "input" : {"key" : "value" },
158+ }
159+ ),
147160 payload = {"action" : "confirm_tool_call" },
148161 )
149162 yield UiPathRuntimeResult (
@@ -414,16 +427,37 @@ async def stream(
414427 trigger_a = UiPathResumeTrigger (
415428 interrupt_id = "email-confirm" ,
416429 trigger_type = UiPathResumeTriggerType .API ,
430+ api_resume = UiPathApiTrigger (
431+ request = {
432+ "tool_call_id" : "tc-email" ,
433+ "tool_name" : "send_email" ,
434+ "input" : {"to" : "user@example.com" },
435+ }
436+ ),
417437 payload = {"action" : "send_email" , "to" : "user@example.com" },
418438 )
419439 trigger_b = UiPathResumeTrigger (
420440 interrupt_id = "file-delete" ,
421441 trigger_type = UiPathResumeTriggerType .API ,
442+ api_resume = UiPathApiTrigger (
443+ request = {
444+ "tool_call_id" : "tc-file" ,
445+ "tool_name" : "delete_file" ,
446+ "input" : {"path" : "/logs/old.txt" },
447+ }
448+ ),
422449 payload = {"action" : "delete_file" , "path" : "/logs/old.txt" },
423450 )
424451 trigger_c = UiPathResumeTrigger (
425452 interrupt_id = "api-call" ,
426453 trigger_type = UiPathResumeTriggerType .API ,
454+ api_resume = UiPathApiTrigger (
455+ request = {
456+ "tool_call_id" : "tc-api" ,
457+ "tool_name" : "call_api" ,
458+ "input" : {"endpoint" : "/users" },
459+ }
460+ ),
427461 payload = {"action" : "call_api" , "endpoint" : "/users" },
428462 )
429463
@@ -483,11 +517,25 @@ async def stream(
483517 trigger_a = UiPathResumeTrigger (
484518 interrupt_id = "email-confirm" ,
485519 trigger_type = UiPathResumeTriggerType .API ,
520+ api_resume = UiPathApiTrigger (
521+ request = {
522+ "tool_call_id" : "tc-email" ,
523+ "tool_name" : "send_email" ,
524+ "input" : {"to" : "user@example.com" },
525+ }
526+ ),
486527 payload = {"action" : "send_email" },
487528 )
488529 trigger_b = UiPathResumeTrigger (
489530 interrupt_id = "file-delete" ,
490531 trigger_type = UiPathResumeTriggerType .API ,
532+ api_resume = UiPathApiTrigger (
533+ request = {
534+ "tool_call_id" : "tc-file" ,
535+ "tool_name" : "delete_file" ,
536+ "input" : {"path" : "/logs/old.txt" },
537+ }
538+ ),
491539 payload = {"action" : "delete_file" },
492540 )
493541 trigger_c = UiPathResumeTrigger (
@@ -612,3 +660,101 @@ async def test_chat_runtime_filters_non_api_triggers():
612660 assert emit_calls [0 ][0 ][0 ].trigger_type == UiPathResumeTriggerType .API
613661 assert emit_calls [1 ][0 ][0 ].interrupt_id == "file-delete"
614662 assert emit_calls [1 ][0 ][0 ].trigger_type == UiPathResumeTriggerType .API
663+
664+
665+ class TestParseConfirmation :
666+ def test_approved_confirmation (self ):
667+ result = _parse_confirmation ({"approved" : True })
668+ assert result is not None
669+ assert result .approved is True
670+ assert result .input is None
671+
672+ def test_rejected_confirmation (self ):
673+ result = _parse_confirmation ({"approved" : False })
674+ assert result is not None
675+ assert result .approved is False
676+
677+ def test_confirmation_with_modified_input (self ):
678+ result = _parse_confirmation ({"approved" : True , "input" : {"key" : "new_val" }})
679+ assert result is not None
680+ assert result .approved is True
681+ assert result .input == {"key" : "new_val" }
682+
683+ def test_end_tool_call_returns_none (self ):
684+ result = _parse_confirmation ({"output" : {"result" : 42 }, "isError" : False })
685+ assert result is None
686+
687+ def test_empty_dict_returns_none (self ):
688+ result = _parse_confirmation ({})
689+ assert result is None
690+
691+ def test_unrelated_data_returns_none (self ):
692+ result = _parse_confirmation ({"foo" : "bar" , "baz" : 123 })
693+ assert result is None
694+
695+
696+ @pytest .mark .asyncio
697+ async def test_confirmation_approved_emits_executing_tool_call_event ():
698+ """Approved confirmation should emit executingToolCall with original input."""
699+ runtime_impl = SuspendingMockRuntime (suspend_at_message = 0 )
700+ bridge = make_chat_bridge_mock ()
701+ cast (AsyncMock , bridge .wait_for_resume ).return_value = {"approved" : True }
702+
703+ chat_runtime = UiPathChatRuntime (delegate = runtime_impl , chat_bridge = bridge )
704+ await chat_runtime .execute ({})
705+ await chat_runtime .dispose ()
706+
707+ cast (AsyncMock , bridge .emit_executing_tool_call_event ).assert_awaited_once_with (
708+ tool_call_id = "tc-1" ,
709+ tool_input = {"key" : "value" },
710+ )
711+
712+
713+ @pytest .mark .asyncio
714+ async def test_confirmation_approved_with_modified_input ():
715+ """Approved confirmation with modified input should use confirmation input."""
716+ runtime_impl = SuspendingMockRuntime (suspend_at_message = 0 )
717+ bridge = make_chat_bridge_mock ()
718+ cast (AsyncMock , bridge .wait_for_resume ).return_value = {
719+ "approved" : True ,
720+ "input" : {"key" : "modified" },
721+ }
722+
723+ chat_runtime = UiPathChatRuntime (delegate = runtime_impl , chat_bridge = bridge )
724+ await chat_runtime .execute ({})
725+ await chat_runtime .dispose ()
726+
727+ cast (AsyncMock , bridge .emit_executing_tool_call_event ).assert_awaited_once_with (
728+ tool_call_id = "tc-1" ,
729+ tool_input = {"key" : "modified" },
730+ )
731+
732+
733+ @pytest .mark .asyncio
734+ async def test_confirmation_rejected_does_not_emit_executing ():
735+ """Rejected confirmation should not emit executingToolCall."""
736+ runtime_impl = SuspendingMockRuntime (suspend_at_message = 0 )
737+ bridge = make_chat_bridge_mock ()
738+ cast (AsyncMock , bridge .wait_for_resume ).return_value = {"approved" : False }
739+
740+ chat_runtime = UiPathChatRuntime (delegate = runtime_impl , chat_bridge = bridge )
741+ await chat_runtime .execute ({})
742+ await chat_runtime .dispose ()
743+
744+ cast (AsyncMock , bridge .emit_executing_tool_call_event ).assert_not_awaited ()
745+
746+
747+ @pytest .mark .asyncio
748+ async def test_end_tool_call_does_not_emit_executing ():
749+ """endToolCall resume data should not emit executingToolCall."""
750+ runtime_impl = SuspendingMockRuntime (suspend_at_message = 0 )
751+ bridge = make_chat_bridge_mock ()
752+ cast (AsyncMock , bridge .wait_for_resume ).return_value = {
753+ "output" : {"result" : 42 },
754+ }
755+
756+ chat_runtime = UiPathChatRuntime (delegate = runtime_impl , chat_bridge = bridge )
757+ await chat_runtime .execute ({})
758+ await chat_runtime .dispose ()
759+
760+ cast (AsyncMock , bridge .emit_executing_tool_call_event ).assert_not_awaited ()
0 commit comments