cua-agent 0.1.41__tar.gz → 0.1.43__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cua-agent might be problematic. Click here for more details.
- {cua_agent-0.1.41 → cua_agent-0.1.43}/PKG-INFO +1 -1
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/base.py +9 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/loop.py +28 -2
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/loop.py +82 -9
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/loop.py +31 -5
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/loop.py +150 -75
- {cua_agent-0.1.41 → cua_agent-0.1.43}/pyproject.toml +3 -3
- {cua_agent-0.1.41 → cua_agent-0.1.43}/README.md +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/agent.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/callbacks.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/experiment.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/factory.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/messages.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/provider_config.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/telemetry.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/base.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/bash.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/collection.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/computer.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/edit.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools/manager.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/tools.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/types.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/core/visualization.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/api/client.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/api/logging.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/api_handler.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/callbacks/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/callbacks/manager.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/prompts.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/response_handler.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/base.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/bash.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/collection.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/computer.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/edit.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/manager.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/tools/run.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/types.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/anthropic/utils.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/api_handler.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/clients/anthropic.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/clients/base.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/clients/oaicompat.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/clients/ollama.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/clients/openai.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/clients/utils.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/image_utils.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/parser.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/prompts.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/tools/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/tools/base.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/tools/bash.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/tools/computer.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/tools/manager.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/omni/utils.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/api_handler.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/response_handler.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/tools/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/tools/base.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/tools/computer.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/tools/manager.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/types.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/openai/utils.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/clients/base.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/clients/mlxvlm.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/clients/oaicompat.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/prompts.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/tools/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/tools/computer.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/tools/manager.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/providers/uitars/utils.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/telemetry.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/ui/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/ui/gradio/__init__.py +0 -0
- {cua_agent-0.1.41 → cua_agent-0.1.43}/agent/ui/gradio/app.py +0 -0
|
@@ -131,6 +131,15 @@ class BaseLoop(ABC):
|
|
|
131
131
|
An async generator that yields agent responses
|
|
132
132
|
"""
|
|
133
133
|
raise NotImplementedError
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
async def cancel(self) -> None:
|
|
137
|
+
"""Cancel the currently running agent loop task.
|
|
138
|
+
|
|
139
|
+
This method should stop any ongoing processing in the agent loop
|
|
140
|
+
and clean up resources appropriately.
|
|
141
|
+
"""
|
|
142
|
+
raise NotImplementedError
|
|
134
143
|
|
|
135
144
|
###########################################
|
|
136
145
|
# EXPERIMENT AND TRAJECTORY MANAGEMENT
|
|
@@ -101,6 +101,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
101
101
|
self.tool_manager = None
|
|
102
102
|
self.callback_manager = None
|
|
103
103
|
self.queue = asyncio.Queue() # Initialize queue
|
|
104
|
+
self.loop_task = None # Store the loop task for cancellation
|
|
104
105
|
|
|
105
106
|
# Initialize handlers
|
|
106
107
|
self.api_handler = AnthropicAPIHandler(self)
|
|
@@ -169,7 +170,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
169
170
|
logger.info("Client initialized successfully")
|
|
170
171
|
|
|
171
172
|
# Start loop in background task
|
|
172
|
-
loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
173
|
+
self.loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
173
174
|
|
|
174
175
|
# Process and yield messages as they arrive
|
|
175
176
|
while True:
|
|
@@ -184,7 +185,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
184
185
|
continue
|
|
185
186
|
|
|
186
187
|
# Wait for loop to complete
|
|
187
|
-
await loop_task
|
|
188
|
+
await self.loop_task
|
|
188
189
|
|
|
189
190
|
# Send completion message
|
|
190
191
|
yield {
|
|
@@ -200,6 +201,31 @@ class AnthropicLoop(BaseLoop):
|
|
|
200
201
|
"content": f"Error: {str(e)}",
|
|
201
202
|
"metadata": {"title": "❌ Error"},
|
|
202
203
|
}
|
|
204
|
+
|
|
205
|
+
async def cancel(self) -> None:
|
|
206
|
+
"""Cancel the currently running agent loop task.
|
|
207
|
+
|
|
208
|
+
This method stops the ongoing processing in the agent loop
|
|
209
|
+
by cancelling the loop_task if it exists and is running.
|
|
210
|
+
"""
|
|
211
|
+
if self.loop_task and not self.loop_task.done():
|
|
212
|
+
logger.info("Cancelling Anthropic loop task")
|
|
213
|
+
self.loop_task.cancel()
|
|
214
|
+
try:
|
|
215
|
+
# Wait for the task to be cancelled with a timeout
|
|
216
|
+
await asyncio.wait_for(self.loop_task, timeout=2.0)
|
|
217
|
+
except asyncio.TimeoutError:
|
|
218
|
+
logger.warning("Timeout while waiting for loop task to cancel")
|
|
219
|
+
except asyncio.CancelledError:
|
|
220
|
+
logger.info("Loop task cancelled successfully")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Error while cancelling loop task: {str(e)}")
|
|
223
|
+
finally:
|
|
224
|
+
# Put None in the queue to signal any waiting consumers to stop
|
|
225
|
+
await self.queue.put(None)
|
|
226
|
+
logger.info("Anthropic loop task cancelled")
|
|
227
|
+
else:
|
|
228
|
+
logger.info("No active Anthropic loop task to cancel")
|
|
203
229
|
|
|
204
230
|
###########################################
|
|
205
231
|
# AGENT LOOP IMPLEMENTATION
|
|
@@ -105,6 +105,7 @@ class OmniLoop(BaseLoop):
|
|
|
105
105
|
# Set API client attributes
|
|
106
106
|
self.client = None
|
|
107
107
|
self.retry_count = 0
|
|
108
|
+
self.loop_task = None # Store the loop task for cancellation
|
|
108
109
|
|
|
109
110
|
# Initialize handlers
|
|
110
111
|
self.api_handler = OmniAPIHandler(loop=self)
|
|
@@ -580,10 +581,55 @@ class OmniLoop(BaseLoop):
|
|
|
580
581
|
Yields:
|
|
581
582
|
Agent response format
|
|
582
583
|
"""
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
584
|
+
try:
|
|
585
|
+
logger.info(f"Starting OmniLoop run with {len(messages)} messages")
|
|
586
|
+
|
|
587
|
+
# Initialize the message manager with the provided messages
|
|
588
|
+
self.message_manager.messages = messages.copy()
|
|
589
|
+
|
|
590
|
+
# Create queue for response streaming
|
|
591
|
+
queue = asyncio.Queue()
|
|
592
|
+
|
|
593
|
+
# Start loop in background task
|
|
594
|
+
self.loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
595
|
+
|
|
596
|
+
# Process and yield messages as they arrive
|
|
597
|
+
while True:
|
|
598
|
+
try:
|
|
599
|
+
item = await queue.get()
|
|
600
|
+
if item is None: # Stop signal
|
|
601
|
+
break
|
|
602
|
+
yield item
|
|
603
|
+
queue.task_done()
|
|
604
|
+
except Exception as e:
|
|
605
|
+
logger.error(f"Error processing queue item: {str(e)}")
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
# Wait for loop to complete
|
|
609
|
+
await self.loop_task
|
|
586
610
|
|
|
611
|
+
# Send completion message
|
|
612
|
+
yield {
|
|
613
|
+
"role": "assistant",
|
|
614
|
+
"content": "Task completed successfully.",
|
|
615
|
+
"metadata": {"title": "✅ Complete"},
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
except Exception as e:
|
|
619
|
+
logger.error(f"Error in run method: {str(e)}")
|
|
620
|
+
yield {
|
|
621
|
+
"role": "assistant",
|
|
622
|
+
"content": f"Error: {str(e)}",
|
|
623
|
+
"metadata": {"title": "❌ Error"},
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) -> None:
|
|
627
|
+
"""Internal method to run the agent loop with provided messages.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
queue: Queue to put responses into
|
|
631
|
+
messages: List of messages in standard OpenAI format
|
|
632
|
+
"""
|
|
587
633
|
# Continue running until explicitly told to stop
|
|
588
634
|
running = True
|
|
589
635
|
turn_created = False
|
|
@@ -673,8 +719,8 @@ class OmniLoop(BaseLoop):
|
|
|
673
719
|
# Log standardized response for ease of parsing
|
|
674
720
|
self._log_api_call("agent_response", request=None, response=openai_compatible_response)
|
|
675
721
|
|
|
676
|
-
#
|
|
677
|
-
|
|
722
|
+
# Put the response in the queue
|
|
723
|
+
await queue.put(openai_compatible_response)
|
|
678
724
|
|
|
679
725
|
# Check if we should continue this conversation
|
|
680
726
|
running = should_continue
|
|
@@ -688,20 +734,47 @@ class OmniLoop(BaseLoop):
|
|
|
688
734
|
|
|
689
735
|
except Exception as e:
|
|
690
736
|
attempt += 1
|
|
691
|
-
error_msg = f"Error in
|
|
737
|
+
error_msg = f"Error in _run_loop method (attempt {attempt}/{max_attempts}): {str(e)}"
|
|
692
738
|
logger.error(error_msg)
|
|
693
739
|
|
|
694
740
|
# If this is our last attempt, provide more info about the error
|
|
695
741
|
if attempt >= max_attempts:
|
|
696
742
|
logger.error(f"Maximum retry attempts reached. Last error was: {str(e)}")
|
|
697
743
|
|
|
698
|
-
|
|
699
|
-
"
|
|
744
|
+
await queue.put({
|
|
745
|
+
"role": "assistant",
|
|
746
|
+
"content": f"Error: {str(e)}",
|
|
700
747
|
"metadata": {"title": "❌ Error"},
|
|
701
|
-
}
|
|
748
|
+
})
|
|
702
749
|
|
|
703
750
|
# Create a brief delay before retrying
|
|
704
751
|
await asyncio.sleep(1)
|
|
752
|
+
finally:
|
|
753
|
+
# Signal that we're done
|
|
754
|
+
await queue.put(None)
|
|
755
|
+
|
|
756
|
+
async def cancel(self) -> None:
|
|
757
|
+
"""Cancel the currently running agent loop task.
|
|
758
|
+
|
|
759
|
+
This method stops the ongoing processing in the agent loop
|
|
760
|
+
by cancelling the loop_task if it exists and is running.
|
|
761
|
+
"""
|
|
762
|
+
if self.loop_task and not self.loop_task.done():
|
|
763
|
+
logger.info("Cancelling Omni loop task")
|
|
764
|
+
self.loop_task.cancel()
|
|
765
|
+
try:
|
|
766
|
+
# Wait for the task to be cancelled with a timeout
|
|
767
|
+
await asyncio.wait_for(self.loop_task, timeout=2.0)
|
|
768
|
+
except asyncio.TimeoutError:
|
|
769
|
+
logger.warning("Timeout while waiting for loop task to cancel")
|
|
770
|
+
except asyncio.CancelledError:
|
|
771
|
+
logger.info("Loop task cancelled successfully")
|
|
772
|
+
except Exception as e:
|
|
773
|
+
logger.error(f"Error while cancelling loop task: {str(e)}")
|
|
774
|
+
finally:
|
|
775
|
+
logger.info("Omni loop task cancelled")
|
|
776
|
+
else:
|
|
777
|
+
logger.info("No active Omni loop task to cancel")
|
|
705
778
|
|
|
706
779
|
async def process_model_response(self, response_text: str) -> Optional[Dict[str, Any]]:
|
|
707
780
|
"""Process model response to extract tool calls.
|
|
@@ -87,6 +87,7 @@ class OpenAILoop(BaseLoop):
|
|
|
87
87
|
self.acknowledge_safety_check_callback = acknowledge_safety_check_callback
|
|
88
88
|
self.queue = asyncio.Queue() # Initialize queue
|
|
89
89
|
self.last_response_id = None # Store the last response ID across runs
|
|
90
|
+
self.loop_task = None # Store the loop task for cancellation
|
|
90
91
|
|
|
91
92
|
# Initialize handlers
|
|
92
93
|
self.api_handler = OpenAIAPIHandler(self)
|
|
@@ -132,28 +133,28 @@ class OpenAILoop(BaseLoop):
|
|
|
132
133
|
logger.info("Starting OpenAI loop run")
|
|
133
134
|
|
|
134
135
|
# Create queue for response streaming
|
|
135
|
-
queue = asyncio.Queue()
|
|
136
|
+
self.queue = asyncio.Queue()
|
|
136
137
|
|
|
137
138
|
# Ensure tool manager is initialized
|
|
138
139
|
await self.tool_manager.initialize()
|
|
139
140
|
|
|
140
141
|
# Start loop in background task
|
|
141
|
-
loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
142
|
+
self.loop_task = asyncio.create_task(self._run_loop(self.queue, messages))
|
|
142
143
|
|
|
143
144
|
# Process and yield messages as they arrive
|
|
144
145
|
while True:
|
|
145
146
|
try:
|
|
146
|
-
item = await queue.get()
|
|
147
|
+
item = await self.queue.get()
|
|
147
148
|
if item is None: # Stop signal
|
|
148
149
|
break
|
|
149
150
|
yield item
|
|
150
|
-
queue.task_done()
|
|
151
|
+
self.queue.task_done()
|
|
151
152
|
except Exception as e:
|
|
152
153
|
logger.error(f"Error processing queue item: {str(e)}")
|
|
153
154
|
continue
|
|
154
155
|
|
|
155
156
|
# Wait for loop to complete
|
|
156
|
-
await loop_task
|
|
157
|
+
await self.loop_task
|
|
157
158
|
|
|
158
159
|
# Send completion message
|
|
159
160
|
yield {
|
|
@@ -169,6 +170,31 @@ class OpenAILoop(BaseLoop):
|
|
|
169
170
|
"content": f"Error: {str(e)}",
|
|
170
171
|
"metadata": {"title": "❌ Error"},
|
|
171
172
|
}
|
|
173
|
+
|
|
174
|
+
async def cancel(self) -> None:
|
|
175
|
+
"""Cancel the currently running agent loop task.
|
|
176
|
+
|
|
177
|
+
This method stops the ongoing processing in the agent loop
|
|
178
|
+
by cancelling the loop_task if it exists and is running.
|
|
179
|
+
"""
|
|
180
|
+
if self.loop_task and not self.loop_task.done():
|
|
181
|
+
logger.info("Cancelling OpenAI loop task")
|
|
182
|
+
self.loop_task.cancel()
|
|
183
|
+
try:
|
|
184
|
+
# Wait for the task to be cancelled with a timeout
|
|
185
|
+
await asyncio.wait_for(self.loop_task, timeout=2.0)
|
|
186
|
+
except asyncio.TimeoutError:
|
|
187
|
+
logger.warning("Timeout while waiting for loop task to cancel")
|
|
188
|
+
except asyncio.CancelledError:
|
|
189
|
+
logger.info("Loop task cancelled successfully")
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error while cancelling loop task: {str(e)}")
|
|
192
|
+
finally:
|
|
193
|
+
# Put None in the queue to signal any waiting consumers to stop
|
|
194
|
+
await self.queue.put(None)
|
|
195
|
+
logger.info("OpenAI loop task cancelled")
|
|
196
|
+
else:
|
|
197
|
+
logger.info("No active OpenAI loop task to cancel")
|
|
172
198
|
|
|
173
199
|
###########################################
|
|
174
200
|
# AGENT LOOP IMPLEMENTATION
|
|
@@ -93,6 +93,7 @@ class UITARSLoop(BaseLoop):
|
|
|
93
93
|
# Set API client attributes
|
|
94
94
|
self.client = None
|
|
95
95
|
self.retry_count = 0
|
|
96
|
+
self.loop_task = None # Store the loop task for cancellation
|
|
96
97
|
|
|
97
98
|
# Initialize visualization helper
|
|
98
99
|
self.viz_helper = VisualizationHelper(agent=self)
|
|
@@ -462,10 +463,55 @@ class UITARSLoop(BaseLoop):
|
|
|
462
463
|
Yields:
|
|
463
464
|
Agent response format
|
|
464
465
|
"""
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
466
|
+
try:
|
|
467
|
+
logger.info(f"Starting UITARSLoop run with {len(messages)} messages")
|
|
468
|
+
|
|
469
|
+
# Initialize the message manager with the provided messages
|
|
470
|
+
self.message_manager.messages = messages.copy()
|
|
471
|
+
|
|
472
|
+
# Create queue for response streaming
|
|
473
|
+
queue = asyncio.Queue()
|
|
474
|
+
|
|
475
|
+
# Start loop in background task
|
|
476
|
+
self.loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
477
|
+
|
|
478
|
+
# Process and yield messages as they arrive
|
|
479
|
+
while True:
|
|
480
|
+
try:
|
|
481
|
+
item = await queue.get()
|
|
482
|
+
if item is None: # Stop signal
|
|
483
|
+
break
|
|
484
|
+
yield item
|
|
485
|
+
queue.task_done()
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Error processing queue item: {str(e)}")
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
# Wait for loop to complete
|
|
491
|
+
await self.loop_task
|
|
468
492
|
|
|
493
|
+
# Send completion message
|
|
494
|
+
yield {
|
|
495
|
+
"role": "assistant",
|
|
496
|
+
"content": "Task completed successfully.",
|
|
497
|
+
"metadata": {"title": "✅ Complete"},
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.error(f"Error in run method: {str(e)}")
|
|
502
|
+
yield {
|
|
503
|
+
"role": "assistant",
|
|
504
|
+
"content": f"Error: {str(e)}",
|
|
505
|
+
"metadata": {"title": "❌ Error"},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) -> None:
|
|
509
|
+
"""Internal method to run the agent loop with provided messages.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
queue: Queue to put responses into
|
|
513
|
+
messages: List of messages in standard OpenAI format
|
|
514
|
+
"""
|
|
469
515
|
# Continue running until explicitly told to stop
|
|
470
516
|
running = True
|
|
471
517
|
turn_created = False
|
|
@@ -475,88 +521,117 @@ class UITARSLoop(BaseLoop):
|
|
|
475
521
|
attempt = 0
|
|
476
522
|
max_attempts = 3
|
|
477
523
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
524
|
+
try:
|
|
525
|
+
while running and attempt < max_attempts:
|
|
526
|
+
try:
|
|
527
|
+
# Create a new turn directory if it's not already created
|
|
528
|
+
if not turn_created:
|
|
529
|
+
self._create_turn_dir()
|
|
530
|
+
turn_created = True
|
|
484
531
|
|
|
485
|
-
|
|
486
|
-
if self.client is None:
|
|
487
|
-
logger.info("Initializing client...")
|
|
488
|
-
await self.initialize_client()
|
|
532
|
+
# Ensure client is initialized
|
|
489
533
|
if self.client is None:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
534
|
+
logger.info("Initializing client...")
|
|
535
|
+
await self.initialize_client()
|
|
536
|
+
if self.client is None:
|
|
537
|
+
raise RuntimeError("Failed to initialize client")
|
|
538
|
+
logger.info("Client initialized successfully")
|
|
539
|
+
|
|
540
|
+
# Get current screen
|
|
541
|
+
base64_screenshot = await self._get_current_screen()
|
|
542
|
+
|
|
543
|
+
# Add screenshot to message history
|
|
544
|
+
self.message_manager.add_user_message(
|
|
545
|
+
[
|
|
546
|
+
{
|
|
547
|
+
"type": "image_url",
|
|
548
|
+
"image_url": {"url": f"data:image/png;base64,{base64_screenshot}"},
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
)
|
|
552
|
+
logger.info("Added screenshot to message history")
|
|
506
553
|
|
|
507
|
-
|
|
508
|
-
|
|
554
|
+
# Get system prompt
|
|
555
|
+
system_prompt = self._get_system_prompt()
|
|
509
556
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
557
|
+
# Make API call with retries
|
|
558
|
+
response = await self._make_api_call(
|
|
559
|
+
self.message_manager.messages, system_prompt
|
|
560
|
+
)
|
|
514
561
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
562
|
+
# Handle the response (may execute actions)
|
|
563
|
+
# Returns: (should_continue, action_screenshot_saved)
|
|
564
|
+
should_continue, new_screenshot_saved = await self._handle_response(
|
|
565
|
+
response, self.message_manager.messages
|
|
566
|
+
)
|
|
520
567
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
568
|
+
# Update whether an action screenshot was saved this turn
|
|
569
|
+
action_screenshot_saved = action_screenshot_saved or new_screenshot_saved
|
|
570
|
+
|
|
571
|
+
agent_response = await to_agent_response_format(
|
|
572
|
+
response,
|
|
573
|
+
messages,
|
|
574
|
+
model=self.model,
|
|
575
|
+
)
|
|
576
|
+
# Log standardized response for ease of parsing
|
|
577
|
+
self._log_api_call("agent_response", request=None, response=agent_response)
|
|
578
|
+
|
|
579
|
+
# Put the response in the queue
|
|
580
|
+
await queue.put(agent_response)
|
|
581
|
+
|
|
582
|
+
# Check if we should continue this conversation
|
|
583
|
+
running = should_continue
|
|
535
584
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
585
|
+
# Create a new turn directory if we're continuing
|
|
586
|
+
if running:
|
|
587
|
+
turn_created = False
|
|
539
588
|
|
|
540
|
-
|
|
541
|
-
|
|
589
|
+
# Reset attempt counter on success
|
|
590
|
+
attempt = 0
|
|
542
591
|
|
|
592
|
+
except Exception as e:
|
|
593
|
+
attempt += 1
|
|
594
|
+
error_msg = f"Error in run method (attempt {attempt}/{max_attempts}): {str(e)}"
|
|
595
|
+
logger.error(error_msg)
|
|
596
|
+
|
|
597
|
+
# If this is our last attempt, provide more info about the error
|
|
598
|
+
if attempt >= max_attempts:
|
|
599
|
+
logger.error(f"Maximum retry attempts reached. Last error was: {str(e)}")
|
|
600
|
+
|
|
601
|
+
await queue.put({
|
|
602
|
+
"role": "assistant",
|
|
603
|
+
"content": f"Error: {str(e)}",
|
|
604
|
+
"metadata": {"title": "❌ Error"},
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
# Create a brief delay before retrying
|
|
608
|
+
await asyncio.sleep(1)
|
|
609
|
+
finally:
|
|
610
|
+
# Signal that we're done
|
|
611
|
+
await queue.put(None)
|
|
612
|
+
|
|
613
|
+
async def cancel(self) -> None:
|
|
614
|
+
"""Cancel the currently running agent loop task.
|
|
615
|
+
|
|
616
|
+
This method stops the ongoing processing in the agent loop
|
|
617
|
+
by cancelling the loop_task if it exists and is running.
|
|
618
|
+
"""
|
|
619
|
+
if self.loop_task and not self.loop_task.done():
|
|
620
|
+
logger.info("Cancelling UITARS loop task")
|
|
621
|
+
self.loop_task.cancel()
|
|
622
|
+
try:
|
|
623
|
+
# Wait for the task to be cancelled with a timeout
|
|
624
|
+
await asyncio.wait_for(self.loop_task, timeout=2.0)
|
|
625
|
+
except asyncio.TimeoutError:
|
|
626
|
+
logger.warning("Timeout while waiting for loop task to cancel")
|
|
627
|
+
except asyncio.CancelledError:
|
|
628
|
+
logger.info("Loop task cancelled successfully")
|
|
543
629
|
except Exception as e:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
logger.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if attempt >= max_attempts:
|
|
550
|
-
logger.error(f"Maximum retry attempts reached. Last error was: {str(e)}")
|
|
551
|
-
|
|
552
|
-
yield {
|
|
553
|
-
"role": "assistant",
|
|
554
|
-
"content": f"Error: {str(e)}",
|
|
555
|
-
"metadata": {"title": "❌ Error"},
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
# Create a brief delay before retrying
|
|
559
|
-
await asyncio.sleep(1)
|
|
630
|
+
logger.error(f"Error while cancelling loop task: {str(e)}")
|
|
631
|
+
finally:
|
|
632
|
+
logger.info("UITARS loop task cancelled")
|
|
633
|
+
else:
|
|
634
|
+
logger.info("No active UITARS loop task to cancel")
|
|
560
635
|
|
|
561
636
|
###########################################
|
|
562
637
|
# UTILITY METHODS
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "cua-agent"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.43"
|
|
10
10
|
description = "CUA (Computer Use) Agent for AI-driven computer interaction"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [
|
|
@@ -109,7 +109,7 @@ target-version = [
|
|
|
109
109
|
|
|
110
110
|
[tool.ruff]
|
|
111
111
|
line-length = 100
|
|
112
|
-
target-version = "0.1.
|
|
112
|
+
target-version = "0.1.43"
|
|
113
113
|
select = [
|
|
114
114
|
"E",
|
|
115
115
|
"F",
|
|
@@ -123,7 +123,7 @@ docstring-code-format = true
|
|
|
123
123
|
|
|
124
124
|
[tool.mypy]
|
|
125
125
|
strict = true
|
|
126
|
-
python_version = "0.1.
|
|
126
|
+
python_version = "0.1.43"
|
|
127
127
|
ignore_missing_imports = true
|
|
128
128
|
disallow_untyped_defs = true
|
|
129
129
|
check_untyped_defs = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|