cua-agent 0.4.17__py3-none-any.whl → 0.4.18__py3-none-any.whl

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.

@@ -1,373 +0,0 @@
1
- """HUD ComputerAgent wrapper for OSWorld benchmarking."""
2
-
3
- import logging
4
- from typing import Any, Literal, Optional, Union, List, Dict
5
- import asyncio
6
-
7
- from agent import ComputerAgent as BaseComputerAgent
8
- from agent.responses import make_failed_tool_call_items
9
- from hud.adapters import Adapter
10
- from hud.agent.base import Agent
11
- from hud.utils.common import Observation
12
- from hud.adapters.common.types import LogType
13
- from hud.types import Gym
14
-
15
- from .adapter import ComputerAgentAdapter
16
- from .computer_handler import HUDComputerHandler
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
- BASE_SYSTEM_PROMPT = """
21
- You are an autonomous computer-using agent. Follow these guidelines:
22
-
23
- 1. Be decisive and complete tasks without asking for confirmation unless absolutely necessary.
24
- 2. Use the computer tools to complete the task and do not stop until the task is complete.
25
- 3. Do NOT ask questions like "Should I proceed?" or "Would you like me to continue?" - just proceed with the task.
26
- 4. When you find what you're looking for (e.g., a file to upload), proceed with the action directly.
27
- 5. Only stop when the task is fully complete or if you encounter an error that prevents completion.
28
- 6. Trust that the user wants you to complete the entire task they've requested.
29
- 7. You must say "Task completed" when the task is complete.
30
-
31
- Remember: You have been given permission to complete the requested task autonomously.
32
- """.strip()
33
-
34
- class ComputerAgent(Agent[BaseComputerAgent, dict[str, Any]]):
35
- """
36
- A ComputerAgent wrapper for HUD integration.
37
-
38
- This agent wraps the base ComputerAgent to work with HUD environments,
39
- providing the same interface as OperatorAgent but using ComputerAgent internally.
40
- """
41
-
42
- transfer_gyms: dict[Gym, Gym] = {"qa": "hud-browser"}
43
-
44
- def __init__(
45
- self,
46
- model: str = "anthropic/claude-3-5-sonnet-20241022",
47
- environment: Literal["windows", "mac", "linux", "browser"] = "linux",
48
- adapter: Optional[Adapter] = None,
49
- name: Optional[str] = None,
50
- **kwargs: Any,
51
- ):
52
- """
53
- Initialize the ComputerAgent for HUD.
54
-
55
- Args:
56
- model: The model string for ComputerAgent (e.g., "anthropic/claude-3-5-sonnet-20241022")
57
- environment: The environment type (windows, mac, linux, browser)
58
- adapter: The adapter to use for preprocessing and postprocessing
59
- name: The name of the agent
60
- **kwargs: Additional arguments passed to ComputerAgent
61
- """
62
- # Create adapter if not provided
63
- adapter = adapter or ComputerAgentAdapter()
64
-
65
- if name is None:
66
- name = f"computeragent-{model.split('/')[-1]}"
67
-
68
- # Initialize the base Agent class without client (we'll create it later)
69
- super().__init__(client=None, adapter=adapter, name=name)
70
-
71
- self.model = model
72
- self.environment = environment
73
- self.kwargs = kwargs
74
-
75
- # Default dimensions
76
- self.width = 1024
77
- self.height = 768
78
-
79
- # Update dimensions if adapter is provided
80
- if self.adapter:
81
- self.width = self.adapter.agent_width
82
- self.height = self.adapter.agent_height
83
-
84
- # Create HUD computer handler
85
- self.hud_computer = HUDComputerHandler(
86
- environment=environment,
87
- dimensions=(self.width, self.height)
88
- )
89
-
90
- # Handle trajectory_dir by adding TrajectorySaverCallback
91
- trajectory_dir = kwargs.pop("trajectory_dir", None)
92
- callbacks = kwargs.get("callbacks", [])
93
-
94
- if trajectory_dir:
95
- from agent.callbacks.trajectory_saver import TrajectorySaverCallback
96
- trajectory_callback = TrajectorySaverCallback(trajectory_dir, reset_on_run=False)
97
- callbacks = callbacks + [trajectory_callback]
98
- kwargs["callbacks"] = callbacks
99
-
100
- # Initialize ComputerAgent with HUD computer handler
101
- self.computer_agent = BaseComputerAgent(
102
- model=model,
103
- tools=[self.hud_computer],
104
- **kwargs
105
- )
106
-
107
- # Set the client to the computer_agent for compatibility
108
- self.client = self.computer_agent
109
-
110
- # State tracking
111
- self.conversation_history: List[Dict[str, Any]] = []
112
- self.initial_prompt: Optional[str] = None
113
-
114
- # System prompt for computer use tasks
115
- self.base_system_prompt = BASE_SYSTEM_PROMPT
116
-
117
- async def fetch_response(self, observation: Observation) -> tuple[list[dict[str, Any]], bool]:
118
- """
119
- Fetch a response from ComputerAgent based on the observation.
120
-
121
- Args:
122
- observation: The preprocessed observation, attributes:
123
- screenshot: Base64 encoded PNG string of the screen
124
- text: Text observation, if available
125
-
126
- Returns:
127
- tuple[list[dict[str, Any]], bool, list[LogType] | None]: A tuple containing the list of raw actions,
128
- boolean indicating if the agent believes the task is complete.
129
- """
130
- try:
131
- # Update the computer handler with the current screenshot
132
- if observation.screenshot:
133
- self.hud_computer.update_screenshot(observation.screenshot)
134
-
135
- # Set up action callback to capture actions
136
- captured_actions = []
137
- action_done = False
138
-
139
- async def action_callback(action: Dict[str, Any]) -> None:
140
- """Callback to capture actions from ComputerAgent."""
141
- nonlocal captured_actions, action_done
142
- captured_actions.append(action)
143
-
144
- # Set the action callback
145
- self.hud_computer.set_action_callback(action_callback)
146
-
147
- # Prepare the message for ComputerAgent
148
- if not self.conversation_history:
149
- # First interaction - use the observation text as initial prompt
150
- if observation.text:
151
- self.initial_prompt = observation.text
152
- message = f"{self.base_system_prompt}\n\nTask: {observation.text}"
153
- else:
154
- message = f"{self.base_system_prompt}\n\nPlease analyze the current screen and determine what action to take."
155
-
156
- input_content = [
157
- {"type": "input_text", "text": message}
158
- ]
159
-
160
- # Add screenshot if present
161
- if observation.screenshot:
162
- input_content.append(
163
- {
164
- "type": "input_image",
165
- "image_url": f"data:image/png;base64,{observation.screenshot}",
166
- }
167
- )
168
-
169
- self.conversation_history.append({"role": "user", "content": input_content})
170
- else:
171
- # Subsequent interactions - check if last action was computer_call
172
- # If so, add computer_call_output with screenshot instead of user message
173
- last_computer_calls = []
174
- for msg in reversed(self.conversation_history):
175
- if msg.get("type") == "computer_call":
176
- call_id = msg.get("call_id")
177
- if call_id:
178
- # Check if this call_id already has a computer_call_output
179
- has_output = any(
180
- m.get("type") == "computer_call_output" and m.get("call_id") == call_id
181
- for m in self.conversation_history
182
- )
183
- if not has_output:
184
- last_computer_calls.append(call_id)
185
-
186
- if last_computer_calls:
187
- if not observation.screenshot:
188
- print("No screenshot found, taking screenshot")
189
- screenshot_b64 = await self.hud_computer.screenshot()
190
- # Add computer_call_output for each unresponded computer_call
191
- for call_id in reversed(last_computer_calls): # Maintain order
192
- self.conversation_history.append({
193
- "type": "computer_call_output",
194
- "call_id": call_id,
195
- "output": {
196
- "type": "input_image",
197
- "image_url": f"data:image/png;base64,{screenshot_b64}"
198
- }
199
- })
200
- else:
201
- # No computer_call found, add regular user message
202
- message = "Continue with the task based on the current screen state."
203
- input_content = [
204
- {"type": "input_text", "text": message}
205
- ]
206
-
207
- # Add screenshot if present
208
- if observation.screenshot:
209
- input_content.append(
210
- {
211
- "type": "input_image",
212
- "image_url": f"data:image/png;base64,{observation.screenshot}",
213
- }
214
- )
215
-
216
- self.conversation_history.append({"role": "user", "content": input_content})
217
-
218
- # If the last message is a reasoning message, change it to output_text
219
- if (self.conversation_history and
220
- self.conversation_history[-1].get("type") == "reasoning" and
221
- self.conversation_history[-1].get("summary")):
222
-
223
- reasoning_msg = self.conversation_history[-1]
224
- summary_texts = []
225
-
226
- # Extract all summary_text entries
227
- for summary_item in reasoning_msg["summary"]:
228
- if summary_item.get("type") == "summary_text":
229
- summary_texts.append(summary_item.get("text", ""))
230
-
231
- # Convert to message format with output_text
232
- if summary_texts:
233
- converted_message = {
234
- "type": "message",
235
- "role": "assistant",
236
- "content": [
237
- {
238
- "text": " ".join(summary_texts),
239
- "type": "output_text"
240
- }
241
- ]
242
- }
243
-
244
- # Replace the reasoning message with the converted message
245
- self.conversation_history[-1] = converted_message
246
-
247
- # Run ComputerAgent
248
- try:
249
- new_items = []
250
-
251
- # ComputerAgent.run returns an async generator
252
- try:
253
- async for result in self.computer_agent.run(self.conversation_history, stream=False):
254
- # if the result has computer_call_output, immediately exit
255
- if result.get("output", []) and result.get("output", [])[-1].get("type") == "computer_call_output":
256
- break
257
- # otherwise add agent output to conversation history
258
- new_items += result["output"]
259
- except Exception as e:
260
- # if the last message is reasoning, change it to output_text
261
- if new_items and new_items[-1].get("type") == "reasoning":
262
- new_items[-1] = {
263
- "type": "message",
264
- "role": "assistant",
265
- "content": [
266
- {
267
- "text": new_items[-1].get("summary", [{}])[0].get("text", ""),
268
- "type": "output_text"
269
- }
270
- ]
271
- }
272
- # Check if there are any computer_call items in new_items
273
- computer_calls = [item for item in new_items if item.get("type") == "computer_call"]
274
- if computer_calls:
275
- # Remove computer_call items from new_items
276
- new_items = [item for item in new_items if item.get("type") != "computer_call"]
277
-
278
- # Add failed tool call items for each computer call
279
- for computer_call in computer_calls:
280
- tool_input = computer_call.get("action", {})
281
- call_id = computer_call.get("call_id")
282
- new_items.extend(make_failed_tool_call_items(
283
- tool_name="computer",
284
- tool_kwargs=tool_input,
285
- error_message=repr(e),
286
- call_id=call_id
287
- ))
288
- else:
289
- # add error message to conversation history (fallback for non-computer-call errors)
290
- new_items.append({
291
- "type": "user",
292
- "content": [
293
- {
294
- "type": "input_text",
295
- "text": f"Error during previous attempted action: {repr(e)}"
296
- }
297
- ]
298
- })
299
-
300
- # Check if we captured any actions
301
- if captured_actions:
302
- # Extract reasoning from the conversation history
303
- reasoning = ""
304
- # Look for the latest reasoning message
305
- for msg in reversed(new_items):
306
- if msg.get("type") == "reasoning" and msg.get("summary"):
307
- reasoning = " ".join([s.get("text", "") for s in msg["summary"] if s.get("type") == "summary_text"])
308
- break
309
- elif msg.get("type") == "message" and msg.get("role") == "assistant":
310
- content = msg.get("content", [])
311
- if isinstance(content, list):
312
- reasoning = " ".join([c.get("text", "") for c in content if c.get("type") == "output_text"])
313
- break
314
-
315
- # update conversation history
316
- self.conversation_history += new_items
317
-
318
- # Add reasoning and logs to each action
319
- for action in captured_actions:
320
- action["reasoning"] = reasoning
321
- action["logs"] = {"conversation_length": len(self.conversation_history)}
322
-
323
- return captured_actions, False
324
-
325
- # Check if the last message is "Task completed"
326
- response_text = ""
327
- for msg in reversed(new_items):
328
- if msg.get("type") == "message" and msg.get("role") == "assistant":
329
- content = msg.get("content", [])
330
- for c in content:
331
- if c.get("type") == "output_text":
332
- response_text = c.get("text", response_text)
333
- break
334
- break
335
-
336
- done = "task completed" in response_text.lower()
337
-
338
- # update conversation history
339
- self.conversation_history += new_items
340
-
341
- response_action = {
342
- "type": "response",
343
- "text": response_text,
344
- "reasoning": response_text,
345
- "logs": {"conversation_length": len(self.conversation_history)}
346
- }
347
-
348
- # Check if this indicates task completion or failure
349
- if "task is infeasible" in response_text.lower():
350
- response_action = {"type": "custom", "action": "FAIL"}
351
- done = True
352
-
353
- return [response_action], done
354
- except Exception as e:
355
- logger.error(f"Error running ComputerAgent: {e}")
356
- # Return an error response
357
- error_action = {
358
- "type": "response",
359
- "text": f"Error occurred: {str(e)}",
360
- "reasoning": f"ComputerAgent encountered an error: {str(e)}",
361
- "logs": {"error": str(e)}
362
- }
363
- return [error_action], True
364
-
365
- except Exception as e:
366
- logger.error(f"Error in fetch_response: {e}")
367
- error_action = {
368
- "type": "response",
369
- "text": f"Error in agent processing: {str(e)}",
370
- "reasoning": f"Agent processing error: {str(e)}",
371
- "logs": {"error": str(e)}
372
- }
373
- return [error_action], True
@@ -1,187 +0,0 @@
1
- """HUD Computer Handler for ComputerAgent integration."""
2
-
3
- import base64
4
- from io import BytesIO
5
- from typing import Literal, Optional, Any, Dict, Callable
6
- from PIL import Image
7
-
8
- from agent.computers import AsyncComputerHandler
9
-
10
-
11
- class HUDComputerHandler(AsyncComputerHandler):
12
- """Computer handler that interfaces with HUD environment."""
13
-
14
- def __init__(
15
- self,
16
- environment: Literal["windows", "mac", "linux", "browser"] = "linux",
17
- dimensions: tuple[int, int] = (1024, 768),
18
- screenshot_callback: Optional[Callable] = None,
19
- action_callback: Optional[Callable] = None,
20
- ):
21
- """
22
- Initialize HUD computer handler.
23
-
24
- Args:
25
- environment: The environment type for HUD
26
- dimensions: Screen dimensions as (width, height)
27
- screenshot_callback: Optional callback to get screenshots from HUD environment
28
- action_callback: Optional callback to execute actions in HUD environment
29
- """
30
- super().__init__()
31
- self._environment = environment
32
- self._dimensions = dimensions
33
- self._screenshot_callback = screenshot_callback
34
- self._action_callback = action_callback
35
-
36
- # Store the last screenshot for reuse
37
- self._last_screenshot: Optional[str] = None
38
-
39
- def set_screenshot_callback(self, callback: Callable) -> None:
40
- """Set the screenshot callback."""
41
- self._screenshot_callback = callback
42
-
43
- def set_action_callback(self, callback: Callable) -> None:
44
- """Set the action callback."""
45
- self._action_callback = callback
46
-
47
- def update_screenshot(self, screenshot: str) -> None:
48
- """Update the stored screenshot (base64 string)."""
49
- self._last_screenshot = screenshot
50
-
51
- async def get_environment(self) -> Literal["windows", "mac", "linux", "browser"]:
52
- """Get the current environment type."""
53
- return self._environment # type: ignore
54
-
55
- async def get_dimensions(self) -> tuple[int, int]:
56
- """Get screen dimensions as (width, height)."""
57
- return self._dimensions
58
-
59
- async def screenshot(self) -> str:
60
- """Take a screenshot and return as base64 string."""
61
- if self._screenshot_callback:
62
- screenshot = await self._screenshot_callback()
63
- if isinstance(screenshot, str):
64
- self._last_screenshot = screenshot
65
- return screenshot
66
- elif isinstance(screenshot, Image.Image):
67
- # Convert PIL Image to base64
68
- buffer = BytesIO()
69
- screenshot.save(buffer, format="PNG")
70
- screenshot_b64 = base64.b64encode(buffer.getvalue()).decode()
71
- self._last_screenshot = screenshot_b64
72
- return screenshot_b64
73
- elif isinstance(screenshot, bytes):
74
- screenshot_b64 = base64.b64encode(screenshot).decode()
75
- self._last_screenshot = screenshot_b64
76
- return screenshot_b64
77
-
78
- # Return last screenshot if available, otherwise create a blank one
79
- if self._last_screenshot:
80
- return self._last_screenshot
81
-
82
- # Create a blank screenshot as fallback
83
- blank_image = Image.new('RGB', self._dimensions, color='white')
84
- buffer = BytesIO()
85
- blank_image.save(buffer, format="PNG")
86
- screenshot_b64 = base64.b64encode(buffer.getvalue()).decode()
87
- self._last_screenshot = screenshot_b64
88
- return screenshot_b64
89
-
90
- async def click(self, x: int, y: int, button: str = "left") -> None:
91
- """Click at coordinates with specified button."""
92
- if self._action_callback:
93
- await self._action_callback({
94
- "type": "click",
95
- "x": x,
96
- "y": y,
97
- "button": button
98
- })
99
-
100
- async def double_click(self, x: int, y: int) -> None:
101
- """Double click at coordinates."""
102
- if self._action_callback:
103
- await self._action_callback({
104
- "type": "double_click",
105
- "x": x,
106
- "y": y
107
- })
108
-
109
- async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
110
- """Scroll at coordinates with specified scroll amounts."""
111
- if self._action_callback:
112
- await self._action_callback({
113
- "type": "scroll",
114
- "x": x,
115
- "y": y,
116
- "scroll_x": scroll_x,
117
- "scroll_y": scroll_y
118
- })
119
-
120
- async def type(self, text: str) -> None:
121
- """Type text."""
122
- if self._action_callback:
123
- await self._action_callback({
124
- "type": "type",
125
- "text": text
126
- })
127
-
128
- async def wait(self, ms: int = 1000) -> None:
129
- """Wait for specified milliseconds."""
130
- if self._action_callback:
131
- await self._action_callback({
132
- "type": "wait",
133
- "ms": ms
134
- })
135
-
136
- async def move(self, x: int, y: int) -> None:
137
- """Move cursor to coordinates."""
138
- if self._action_callback:
139
- await self._action_callback({
140
- "type": "move",
141
- "x": x,
142
- "y": y
143
- })
144
-
145
- async def keypress(self, keys: list[str] | str) -> None:
146
- """Press key combination."""
147
- if isinstance(keys, str):
148
- keys = [keys]
149
- if self._action_callback:
150
- await self._action_callback({
151
- "type": "keypress",
152
- "keys": keys
153
- })
154
-
155
- async def drag(self, path: list[dict[str, int]]) -> None:
156
- """Drag along a path of points."""
157
- if self._action_callback:
158
- await self._action_callback({
159
- "type": "drag",
160
- "path": path
161
- })
162
-
163
- async def left_mouse_down(self, x: Optional[int] = None, y: Optional[int] = None) -> None:
164
- """Left mouse down at coordinates."""
165
- if self._action_callback:
166
- await self._action_callback({
167
- "type": "left_mouse_down",
168
- "x": x,
169
- "y": y
170
- })
171
-
172
- async def left_mouse_up(self, x: Optional[int] = None, y: Optional[int] = None) -> None:
173
- """Left mouse up at coordinates."""
174
- if self._action_callback:
175
- await self._action_callback({
176
- "type": "left_mouse_up",
177
- "x": x,
178
- "y": y
179
- })
180
-
181
- async def get_current_url(self) -> str:
182
- """Get the current URL."""
183
- if self._action_callback:
184
- return await self._action_callback({
185
- "type": "get_current_url"
186
- })
187
- return ""