cua-agent 0.1.6__py3-none-any.whl → 0.1.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.
- agent/__init__.py +3 -2
- agent/core/__init__.py +1 -6
- agent/core/{computer_agent.py → agent.py} +31 -76
- agent/core/{loop.py → base.py} +68 -127
- agent/core/factory.py +104 -0
- agent/core/messages.py +279 -125
- agent/core/provider_config.py +15 -0
- agent/core/types.py +45 -0
- agent/core/visualization.py +197 -0
- agent/providers/anthropic/api/client.py +142 -1
- agent/providers/anthropic/api_handler.py +140 -0
- agent/providers/anthropic/callbacks/__init__.py +5 -0
- agent/providers/anthropic/loop.py +207 -221
- agent/providers/anthropic/response_handler.py +226 -0
- agent/providers/anthropic/tools/bash.py +0 -97
- agent/providers/anthropic/utils.py +368 -0
- agent/providers/omni/__init__.py +1 -20
- agent/providers/omni/api_handler.py +42 -0
- agent/providers/omni/clients/anthropic.py +4 -0
- agent/providers/omni/image_utils.py +0 -72
- agent/providers/omni/loop.py +491 -607
- agent/providers/omni/parser.py +58 -4
- agent/providers/omni/tools/__init__.py +25 -7
- agent/providers/omni/tools/base.py +29 -0
- agent/providers/omni/tools/bash.py +43 -38
- agent/providers/omni/tools/computer.py +144 -182
- agent/providers/omni/tools/manager.py +25 -45
- agent/providers/omni/types.py +1 -3
- agent/providers/omni/utils.py +224 -145
- agent/providers/openai/__init__.py +6 -0
- agent/providers/openai/api_handler.py +453 -0
- agent/providers/openai/loop.py +440 -0
- agent/providers/openai/response_handler.py +205 -0
- agent/providers/openai/tools/__init__.py +15 -0
- agent/providers/openai/tools/base.py +79 -0
- agent/providers/openai/tools/computer.py +319 -0
- agent/providers/openai/tools/manager.py +106 -0
- agent/providers/openai/types.py +36 -0
- agent/providers/openai/utils.py +98 -0
- cua_agent-0.1.18.dist-info/METADATA +165 -0
- cua_agent-0.1.18.dist-info/RECORD +73 -0
- agent/README.md +0 -63
- agent/providers/anthropic/messages/manager.py +0 -112
- agent/providers/omni/callbacks.py +0 -78
- agent/providers/omni/clients/groq.py +0 -101
- agent/providers/omni/experiment.py +0 -276
- agent/providers/omni/messages.py +0 -171
- agent/providers/omni/tool_manager.py +0 -91
- agent/providers/omni/visualization.py +0 -130
- agent/types/__init__.py +0 -23
- agent/types/base.py +0 -41
- agent/types/messages.py +0 -36
- cua_agent-0.1.6.dist-info/METADATA +0 -120
- cua_agent-0.1.6.dist-info/RECORD +0 -64
- /agent/{types → core}/tools.py +0 -0
- {cua_agent-0.1.6.dist-info → cua_agent-0.1.18.dist-info}/WHEEL +0 -0
- {cua_agent-0.1.6.dist-info → cua_agent-0.1.18.dist-info}/entry_points.txt +0 -0
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
"""Experiment management for the Cua provider."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import logging
|
|
5
|
-
import copy
|
|
6
|
-
import base64
|
|
7
|
-
from io import BytesIO
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from typing import Any, Dict, List, Optional
|
|
10
|
-
from PIL import Image
|
|
11
|
-
import json
|
|
12
|
-
import time
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ExperimentManager:
|
|
18
|
-
"""Manages experiment directories and logging for the agent."""
|
|
19
|
-
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
base_dir: Optional[str] = None,
|
|
23
|
-
only_n_most_recent_images: Optional[int] = None,
|
|
24
|
-
):
|
|
25
|
-
"""Initialize the experiment manager.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
base_dir: Base directory for saving experiment data
|
|
29
|
-
only_n_most_recent_images: Maximum number of recent screenshots to include in API requests
|
|
30
|
-
"""
|
|
31
|
-
self.base_dir = base_dir
|
|
32
|
-
self.only_n_most_recent_images = only_n_most_recent_images
|
|
33
|
-
self.run_dir = None
|
|
34
|
-
self.current_turn_dir = None
|
|
35
|
-
self.turn_count = 0
|
|
36
|
-
self.screenshot_count = 0
|
|
37
|
-
# Track all screenshots for potential API request inclusion
|
|
38
|
-
self.screenshot_paths = []
|
|
39
|
-
|
|
40
|
-
# Set up experiment directories if base_dir is provided
|
|
41
|
-
if self.base_dir:
|
|
42
|
-
self.setup_experiment_dirs()
|
|
43
|
-
|
|
44
|
-
def setup_experiment_dirs(self) -> None:
|
|
45
|
-
"""Setup the experiment directory structure."""
|
|
46
|
-
if not self.base_dir:
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
# Create base experiments directory if it doesn't exist
|
|
50
|
-
os.makedirs(self.base_dir, exist_ok=True)
|
|
51
|
-
|
|
52
|
-
# Use the base_dir directly as the run_dir
|
|
53
|
-
self.run_dir = self.base_dir
|
|
54
|
-
logger.info(f"Using directory for experiment: {self.run_dir}")
|
|
55
|
-
|
|
56
|
-
# Create first turn directory
|
|
57
|
-
self.create_turn_dir()
|
|
58
|
-
|
|
59
|
-
def create_turn_dir(self) -> None:
|
|
60
|
-
"""Create a new directory for the current turn."""
|
|
61
|
-
if not self.run_dir:
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
self.turn_count += 1
|
|
65
|
-
self.current_turn_dir = os.path.join(self.run_dir, f"turn_{self.turn_count:03d}")
|
|
66
|
-
os.makedirs(self.current_turn_dir, exist_ok=True)
|
|
67
|
-
logger.info(f"Created turn directory: {self.current_turn_dir}")
|
|
68
|
-
|
|
69
|
-
def sanitize_log_data(self, data: Any) -> Any:
|
|
70
|
-
"""Sanitize data for logging by removing large base64 strings.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
data: Data to sanitize (dict, list, or primitive)
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
Sanitized copy of the data
|
|
77
|
-
"""
|
|
78
|
-
if isinstance(data, dict):
|
|
79
|
-
result = copy.deepcopy(data)
|
|
80
|
-
|
|
81
|
-
# Handle nested dictionaries and lists
|
|
82
|
-
for key, value in result.items():
|
|
83
|
-
# Process content arrays that contain image data
|
|
84
|
-
if key == "content" and isinstance(value, list):
|
|
85
|
-
for i, item in enumerate(value):
|
|
86
|
-
if isinstance(item, dict):
|
|
87
|
-
# Handle Anthropic format
|
|
88
|
-
if item.get("type") == "image" and isinstance(item.get("source"), dict):
|
|
89
|
-
source = item["source"]
|
|
90
|
-
if "data" in source and isinstance(source["data"], str):
|
|
91
|
-
# Replace base64 data with a placeholder and length info
|
|
92
|
-
data_len = len(source["data"])
|
|
93
|
-
source["data"] = f"[BASE64_IMAGE_DATA_LENGTH_{data_len}]"
|
|
94
|
-
|
|
95
|
-
# Handle OpenAI format
|
|
96
|
-
elif item.get("type") == "image_url" and isinstance(
|
|
97
|
-
item.get("image_url"), dict
|
|
98
|
-
):
|
|
99
|
-
url_dict = item["image_url"]
|
|
100
|
-
if "url" in url_dict and isinstance(url_dict["url"], str):
|
|
101
|
-
url = url_dict["url"]
|
|
102
|
-
if url.startswith("data:"):
|
|
103
|
-
# Replace base64 data with placeholder
|
|
104
|
-
data_len = len(url)
|
|
105
|
-
url_dict["url"] = f"[BASE64_IMAGE_URL_LENGTH_{data_len}]"
|
|
106
|
-
|
|
107
|
-
# Handle other nested structures recursively
|
|
108
|
-
if isinstance(value, dict):
|
|
109
|
-
result[key] = self.sanitize_log_data(value)
|
|
110
|
-
elif isinstance(value, list):
|
|
111
|
-
result[key] = [self.sanitize_log_data(item) for item in value]
|
|
112
|
-
|
|
113
|
-
return result
|
|
114
|
-
elif isinstance(data, list):
|
|
115
|
-
return [self.sanitize_log_data(item) for item in data]
|
|
116
|
-
else:
|
|
117
|
-
return data
|
|
118
|
-
|
|
119
|
-
def save_debug_image(self, image_data: str, filename: str) -> None:
|
|
120
|
-
"""Save a debug image to the experiment directory.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
image_data: Base64 encoded image data
|
|
124
|
-
filename: Filename to save the image as
|
|
125
|
-
"""
|
|
126
|
-
# Since we no longer want to use the images/ folder, we'll skip this functionality
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
def save_screenshot(self, img_base64: str, action_type: str = "") -> Optional[str]:
|
|
130
|
-
"""Save a screenshot to the experiment directory.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
img_base64: Base64 encoded screenshot
|
|
134
|
-
action_type: Type of action that triggered the screenshot
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
Optional[str]: Path to the saved screenshot, or None if saving failed
|
|
138
|
-
"""
|
|
139
|
-
if not self.current_turn_dir:
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
try:
|
|
143
|
-
# Increment screenshot counter
|
|
144
|
-
self.screenshot_count += 1
|
|
145
|
-
|
|
146
|
-
# Create a descriptive filename
|
|
147
|
-
timestamp = int(time.time() * 1000)
|
|
148
|
-
action_suffix = f"_{action_type}" if action_type else ""
|
|
149
|
-
filename = f"screenshot_{self.screenshot_count:03d}{action_suffix}_{timestamp}.png"
|
|
150
|
-
|
|
151
|
-
# Save directly to the turn directory (no screenshots subdirectory)
|
|
152
|
-
filepath = os.path.join(self.current_turn_dir, filename)
|
|
153
|
-
|
|
154
|
-
# Save the screenshot
|
|
155
|
-
img_data = base64.b64decode(img_base64)
|
|
156
|
-
with open(filepath, "wb") as f:
|
|
157
|
-
f.write(img_data)
|
|
158
|
-
|
|
159
|
-
# Keep track of the file path for reference
|
|
160
|
-
self.screenshot_paths.append(filepath)
|
|
161
|
-
|
|
162
|
-
return filepath
|
|
163
|
-
except Exception as e:
|
|
164
|
-
logger.error(f"Error saving screenshot: {str(e)}")
|
|
165
|
-
return None
|
|
166
|
-
|
|
167
|
-
def should_save_debug_image(self) -> bool:
|
|
168
|
-
"""Determine if debug images should be saved.
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
Boolean indicating if debug images should be saved
|
|
172
|
-
"""
|
|
173
|
-
# We no longer need to save debug images, so always return False
|
|
174
|
-
return False
|
|
175
|
-
|
|
176
|
-
def save_action_visualization(
|
|
177
|
-
self, img: Image.Image, action_name: str, details: str = ""
|
|
178
|
-
) -> str:
|
|
179
|
-
"""Save a visualization of an action.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
img: Image to save
|
|
183
|
-
action_name: Name of the action
|
|
184
|
-
details: Additional details about the action
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
Path to the saved image
|
|
188
|
-
"""
|
|
189
|
-
if not self.current_turn_dir:
|
|
190
|
-
return ""
|
|
191
|
-
|
|
192
|
-
try:
|
|
193
|
-
# Create a descriptive filename
|
|
194
|
-
timestamp = int(time.time() * 1000)
|
|
195
|
-
details_suffix = f"_{details}" if details else ""
|
|
196
|
-
filename = f"vis_{action_name}{details_suffix}_{timestamp}.png"
|
|
197
|
-
|
|
198
|
-
# Save directly to the turn directory (no visualizations subdirectory)
|
|
199
|
-
filepath = os.path.join(self.current_turn_dir, filename)
|
|
200
|
-
|
|
201
|
-
# Save the image
|
|
202
|
-
img.save(filepath)
|
|
203
|
-
|
|
204
|
-
# Keep track of the file path for cleanup
|
|
205
|
-
self.screenshot_paths.append(filepath)
|
|
206
|
-
|
|
207
|
-
return filepath
|
|
208
|
-
except Exception as e:
|
|
209
|
-
logger.error(f"Error saving action visualization: {str(e)}")
|
|
210
|
-
return ""
|
|
211
|
-
|
|
212
|
-
def extract_and_save_images(self, data: Any, prefix: str) -> None:
|
|
213
|
-
"""Extract and save images from response data.
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
data: Response data to extract images from
|
|
217
|
-
prefix: Prefix for saved image filenames
|
|
218
|
-
"""
|
|
219
|
-
# Since we no longer want to save extracted images separately,
|
|
220
|
-
# we'll skip this functionality entirely
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
def log_api_call(
|
|
224
|
-
self,
|
|
225
|
-
call_type: str,
|
|
226
|
-
request: Any,
|
|
227
|
-
provider: str,
|
|
228
|
-
model: str,
|
|
229
|
-
response: Any = None,
|
|
230
|
-
error: Optional[Exception] = None,
|
|
231
|
-
) -> None:
|
|
232
|
-
"""Log API call details to file.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
call_type: Type of API call (e.g., 'request', 'response', 'error')
|
|
236
|
-
request: The API request data
|
|
237
|
-
provider: The AI provider used
|
|
238
|
-
model: The AI model used
|
|
239
|
-
response: Optional API response data
|
|
240
|
-
error: Optional error information
|
|
241
|
-
"""
|
|
242
|
-
if not self.current_turn_dir:
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
# Create a unique filename with timestamp
|
|
247
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
248
|
-
filename = f"api_call_{timestamp}_{call_type}.json"
|
|
249
|
-
filepath = os.path.join(self.current_turn_dir, filename)
|
|
250
|
-
|
|
251
|
-
# Sanitize data to remove large base64 strings
|
|
252
|
-
sanitized_request = self.sanitize_log_data(request)
|
|
253
|
-
sanitized_response = self.sanitize_log_data(response) if response is not None else None
|
|
254
|
-
|
|
255
|
-
# Prepare log data
|
|
256
|
-
log_data = {
|
|
257
|
-
"timestamp": timestamp,
|
|
258
|
-
"provider": provider,
|
|
259
|
-
"model": model,
|
|
260
|
-
"type": call_type,
|
|
261
|
-
"request": sanitized_request,
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if sanitized_response is not None:
|
|
265
|
-
log_data["response"] = sanitized_response
|
|
266
|
-
if error is not None:
|
|
267
|
-
log_data["error"] = str(error)
|
|
268
|
-
|
|
269
|
-
# Write to file
|
|
270
|
-
with open(filepath, "w") as f:
|
|
271
|
-
json.dump(log_data, f, indent=2, default=str)
|
|
272
|
-
|
|
273
|
-
logger.info(f"Logged API {call_type} to {filepath}")
|
|
274
|
-
|
|
275
|
-
except Exception as e:
|
|
276
|
-
logger.error(f"Error logging API call: {str(e)}")
|
agent/providers/omni/messages.py
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"""Omni message manager implementation."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
-
from io import BytesIO
|
|
6
|
-
from PIL import Image
|
|
7
|
-
|
|
8
|
-
from ...core.messages import BaseMessageManager, ImageRetentionConfig
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class OmniMessageManager(BaseMessageManager):
|
|
12
|
-
"""Message manager for multi-provider support."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, config: Optional[ImageRetentionConfig] = None):
|
|
15
|
-
"""Initialize the message manager.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
config: Optional configuration for image retention
|
|
19
|
-
"""
|
|
20
|
-
super().__init__(config)
|
|
21
|
-
self.messages: List[Dict[str, Any]] = []
|
|
22
|
-
self.config = config
|
|
23
|
-
|
|
24
|
-
def add_user_message(self, content: str, images: Optional[List[bytes]] = None) -> None:
|
|
25
|
-
"""Add a user message to the history.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
content: Message content
|
|
29
|
-
images: Optional list of image data
|
|
30
|
-
"""
|
|
31
|
-
# Add images if present
|
|
32
|
-
if images:
|
|
33
|
-
# Initialize with proper typing for mixed content
|
|
34
|
-
message_content: List[Dict[str, Any]] = [{"type": "text", "text": content}]
|
|
35
|
-
|
|
36
|
-
# Add each image
|
|
37
|
-
for img in images:
|
|
38
|
-
message_content.append(
|
|
39
|
-
{
|
|
40
|
-
"type": "image_url",
|
|
41
|
-
"image_url": {
|
|
42
|
-
"url": f"data:image/png;base64,{base64.b64encode(img).decode()}"
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
message = {"role": "user", "content": message_content}
|
|
48
|
-
else:
|
|
49
|
-
# Simple text message
|
|
50
|
-
message = {"role": "user", "content": content}
|
|
51
|
-
|
|
52
|
-
self.messages.append(message)
|
|
53
|
-
|
|
54
|
-
# Apply retention policy
|
|
55
|
-
if self.config and self.config.num_images_to_keep:
|
|
56
|
-
self._apply_image_retention_policy()
|
|
57
|
-
|
|
58
|
-
def add_assistant_message(self, content: str) -> None:
|
|
59
|
-
"""Add an assistant message to the history.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
content: Message content
|
|
63
|
-
"""
|
|
64
|
-
self.messages.append({"role": "assistant", "content": content})
|
|
65
|
-
|
|
66
|
-
def add_system_message(self, content: str) -> None:
|
|
67
|
-
"""Add a system message to the history.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
content: Message content
|
|
71
|
-
"""
|
|
72
|
-
self.messages.append({"role": "system", "content": content})
|
|
73
|
-
|
|
74
|
-
def _apply_image_retention_policy(self) -> None:
|
|
75
|
-
"""Apply image retention policy to message history."""
|
|
76
|
-
if not self.config or not self.config.num_images_to_keep:
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
# Count images from newest to oldest
|
|
80
|
-
image_count = 0
|
|
81
|
-
for message in reversed(self.messages):
|
|
82
|
-
if message["role"] != "user":
|
|
83
|
-
continue
|
|
84
|
-
|
|
85
|
-
# Handle multimodal messages
|
|
86
|
-
if isinstance(message["content"], list):
|
|
87
|
-
new_content = []
|
|
88
|
-
for item in message["content"]:
|
|
89
|
-
if item["type"] == "text":
|
|
90
|
-
new_content.append(item)
|
|
91
|
-
elif item["type"] == "image_url":
|
|
92
|
-
if image_count < self.config.num_images_to_keep:
|
|
93
|
-
new_content.append(item)
|
|
94
|
-
image_count += 1
|
|
95
|
-
message["content"] = new_content
|
|
96
|
-
|
|
97
|
-
def get_formatted_messages(self, provider: str) -> List[Dict[str, Any]]:
|
|
98
|
-
"""Get messages formatted for specific provider.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
provider: Provider name to format messages for
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
List of formatted messages
|
|
105
|
-
"""
|
|
106
|
-
# Set the provider for message formatting
|
|
107
|
-
self.set_provider(provider)
|
|
108
|
-
|
|
109
|
-
if provider == "anthropic":
|
|
110
|
-
return self._format_for_anthropic()
|
|
111
|
-
elif provider == "openai":
|
|
112
|
-
return self._format_for_openai()
|
|
113
|
-
elif provider == "groq":
|
|
114
|
-
return self._format_for_groq()
|
|
115
|
-
elif provider == "qwen":
|
|
116
|
-
return self._format_for_qwen()
|
|
117
|
-
else:
|
|
118
|
-
raise ValueError(f"Unsupported provider: {provider}")
|
|
119
|
-
|
|
120
|
-
def _format_for_anthropic(self) -> List[Dict[str, Any]]:
|
|
121
|
-
"""Format messages for Anthropic API."""
|
|
122
|
-
formatted = []
|
|
123
|
-
for msg in self.messages:
|
|
124
|
-
formatted_msg = {"role": msg["role"]}
|
|
125
|
-
|
|
126
|
-
# Handle multimodal content
|
|
127
|
-
if isinstance(msg["content"], list):
|
|
128
|
-
formatted_msg["content"] = []
|
|
129
|
-
for item in msg["content"]:
|
|
130
|
-
if item["type"] == "text":
|
|
131
|
-
formatted_msg["content"].append({"type": "text", "text": item["text"]})
|
|
132
|
-
elif item["type"] == "image_url":
|
|
133
|
-
formatted_msg["content"].append(
|
|
134
|
-
{
|
|
135
|
-
"type": "image",
|
|
136
|
-
"source": {
|
|
137
|
-
"type": "base64",
|
|
138
|
-
"media_type": "image/png",
|
|
139
|
-
"data": item["image_url"]["url"].split(",")[1],
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
)
|
|
143
|
-
else:
|
|
144
|
-
formatted_msg["content"] = msg["content"]
|
|
145
|
-
|
|
146
|
-
formatted.append(formatted_msg)
|
|
147
|
-
return formatted
|
|
148
|
-
|
|
149
|
-
def _format_for_openai(self) -> List[Dict[str, Any]]:
|
|
150
|
-
"""Format messages for OpenAI API."""
|
|
151
|
-
# OpenAI already uses the same format
|
|
152
|
-
return self.messages
|
|
153
|
-
|
|
154
|
-
def _format_for_groq(self) -> List[Dict[str, Any]]:
|
|
155
|
-
"""Format messages for Groq API."""
|
|
156
|
-
# Groq uses OpenAI-compatible format
|
|
157
|
-
return self.messages
|
|
158
|
-
|
|
159
|
-
def _format_for_qwen(self) -> List[Dict[str, Any]]:
|
|
160
|
-
"""Format messages for Qwen API."""
|
|
161
|
-
formatted = []
|
|
162
|
-
for msg in self.messages:
|
|
163
|
-
if isinstance(msg["content"], list):
|
|
164
|
-
# Convert multimodal content to text-only
|
|
165
|
-
text_content = next(
|
|
166
|
-
(item["text"] for item in msg["content"] if item["type"] == "text"), ""
|
|
167
|
-
)
|
|
168
|
-
formatted.append({"role": msg["role"], "content": text_content})
|
|
169
|
-
else:
|
|
170
|
-
formatted.append(msg)
|
|
171
|
-
return formatted
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# """Omni tool manager implementation."""
|
|
2
|
-
|
|
3
|
-
# from typing import Dict, List, Type, Any
|
|
4
|
-
|
|
5
|
-
# from computer import Computer
|
|
6
|
-
# from ...core.tools import BaseToolManager, BashTool, EditTool
|
|
7
|
-
|
|
8
|
-
# class OmniToolManager(BaseToolManager):
|
|
9
|
-
# """Tool manager for multi-provider support."""
|
|
10
|
-
|
|
11
|
-
# def __init__(self, computer: Computer):
|
|
12
|
-
# """Initialize Omni tool manager.
|
|
13
|
-
|
|
14
|
-
# Args:
|
|
15
|
-
# computer: Computer instance for tools
|
|
16
|
-
# """
|
|
17
|
-
# super().__init__(computer)
|
|
18
|
-
|
|
19
|
-
# def get_anthropic_tools(self) -> List[Dict[str, Any]]:
|
|
20
|
-
# """Get tools formatted for Anthropic API.
|
|
21
|
-
|
|
22
|
-
# Returns:
|
|
23
|
-
# List of tool parameters in Anthropic format
|
|
24
|
-
# """
|
|
25
|
-
# tools: List[Dict[str, Any]] = []
|
|
26
|
-
|
|
27
|
-
# # Map base tools to Anthropic format
|
|
28
|
-
# for tool in self.tools.values():
|
|
29
|
-
# if isinstance(tool, BashTool):
|
|
30
|
-
# tools.append({
|
|
31
|
-
# "type": "bash_20241022",
|
|
32
|
-
# "name": tool.name
|
|
33
|
-
# })
|
|
34
|
-
# elif isinstance(tool, EditTool):
|
|
35
|
-
# tools.append({
|
|
36
|
-
# "type": "text_editor_20241022",
|
|
37
|
-
# "name": "str_replace_editor"
|
|
38
|
-
# })
|
|
39
|
-
|
|
40
|
-
# return tools
|
|
41
|
-
|
|
42
|
-
# def get_openai_tools(self) -> List[Dict]:
|
|
43
|
-
# """Get tools formatted for OpenAI API.
|
|
44
|
-
|
|
45
|
-
# Returns:
|
|
46
|
-
# List of tool parameters in OpenAI format
|
|
47
|
-
# """
|
|
48
|
-
# tools = []
|
|
49
|
-
|
|
50
|
-
# # Map base tools to OpenAI format
|
|
51
|
-
# for tool in self.tools.values():
|
|
52
|
-
# tools.append({
|
|
53
|
-
# "type": "function",
|
|
54
|
-
# "function": tool.get_schema()
|
|
55
|
-
# })
|
|
56
|
-
|
|
57
|
-
# return tools
|
|
58
|
-
|
|
59
|
-
# def get_groq_tools(self) -> List[Dict]:
|
|
60
|
-
# """Get tools formatted for Groq API.
|
|
61
|
-
|
|
62
|
-
# Returns:
|
|
63
|
-
# List of tool parameters in Groq format
|
|
64
|
-
# """
|
|
65
|
-
# tools = []
|
|
66
|
-
|
|
67
|
-
# # Map base tools to Groq format
|
|
68
|
-
# for tool in self.tools.values():
|
|
69
|
-
# tools.append({
|
|
70
|
-
# "type": "function",
|
|
71
|
-
# "function": tool.get_schema()
|
|
72
|
-
# })
|
|
73
|
-
|
|
74
|
-
# return tools
|
|
75
|
-
|
|
76
|
-
# def get_qwen_tools(self) -> List[Dict]:
|
|
77
|
-
# """Get tools formatted for Qwen API.
|
|
78
|
-
|
|
79
|
-
# Returns:
|
|
80
|
-
# List of tool parameters in Qwen format
|
|
81
|
-
# """
|
|
82
|
-
# tools = []
|
|
83
|
-
|
|
84
|
-
# # Map base tools to Qwen format
|
|
85
|
-
# for tool in self.tools.values():
|
|
86
|
-
# tools.append({
|
|
87
|
-
# "type": "function",
|
|
88
|
-
# "function": tool.get_schema()
|
|
89
|
-
# })
|
|
90
|
-
|
|
91
|
-
# return tools
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
"""Visualization utilities for the Cua provider."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import logging
|
|
5
|
-
from io import BytesIO
|
|
6
|
-
from typing import Tuple
|
|
7
|
-
from PIL import Image, ImageDraw
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def visualize_click(x: int, y: int, img_base64: str) -> Image.Image:
|
|
13
|
-
"""Visualize a click action by drawing on the screenshot.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
x: X coordinate of the click
|
|
17
|
-
y: Y coordinate of the click
|
|
18
|
-
img_base64: Base64 encoded image to draw on
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
PIL Image with visualization
|
|
22
|
-
"""
|
|
23
|
-
try:
|
|
24
|
-
# Decode the base64 image
|
|
25
|
-
img_data = base64.b64decode(img_base64)
|
|
26
|
-
img = Image.open(BytesIO(img_data))
|
|
27
|
-
|
|
28
|
-
# Create a drawing context
|
|
29
|
-
draw = ImageDraw.Draw(img)
|
|
30
|
-
|
|
31
|
-
# Draw concentric circles at the click position
|
|
32
|
-
small_radius = 10
|
|
33
|
-
large_radius = 30
|
|
34
|
-
|
|
35
|
-
# Draw filled inner circle
|
|
36
|
-
draw.ellipse(
|
|
37
|
-
[(x - small_radius, y - small_radius), (x + small_radius, y + small_radius)],
|
|
38
|
-
fill="red",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
# Draw outlined outer circle
|
|
42
|
-
draw.ellipse(
|
|
43
|
-
[(x - large_radius, y - large_radius), (x + large_radius, y + large_radius)],
|
|
44
|
-
outline="red",
|
|
45
|
-
width=3,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
return img
|
|
49
|
-
|
|
50
|
-
except Exception as e:
|
|
51
|
-
logger.error(f"Error visualizing click: {str(e)}")
|
|
52
|
-
# Return a blank image in case of error
|
|
53
|
-
return Image.new("RGB", (800, 600), color="white")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def visualize_scroll(direction: str, clicks: int, img_base64: str) -> Image.Image:
|
|
57
|
-
"""Visualize a scroll action by drawing arrows on the screenshot.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
direction: 'up' or 'down'
|
|
61
|
-
clicks: Number of scroll clicks
|
|
62
|
-
img_base64: Base64 encoded image to draw on
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
PIL Image with visualization
|
|
66
|
-
"""
|
|
67
|
-
try:
|
|
68
|
-
# Decode the base64 image
|
|
69
|
-
img_data = base64.b64decode(img_base64)
|
|
70
|
-
img = Image.open(BytesIO(img_data))
|
|
71
|
-
|
|
72
|
-
# Get image dimensions
|
|
73
|
-
width, height = img.size
|
|
74
|
-
|
|
75
|
-
# Create a drawing context
|
|
76
|
-
draw = ImageDraw.Draw(img)
|
|
77
|
-
|
|
78
|
-
# Determine arrow direction and positions
|
|
79
|
-
center_x = width // 2
|
|
80
|
-
arrow_width = 100
|
|
81
|
-
|
|
82
|
-
if direction.lower() == "up":
|
|
83
|
-
# Draw up arrow in the middle of the screen
|
|
84
|
-
arrow_y = height // 2
|
|
85
|
-
# Arrow points
|
|
86
|
-
points = [
|
|
87
|
-
(center_x, arrow_y - 50), # Top point
|
|
88
|
-
(center_x - arrow_width // 2, arrow_y + 50), # Bottom left
|
|
89
|
-
(center_x + arrow_width // 2, arrow_y + 50), # Bottom right
|
|
90
|
-
]
|
|
91
|
-
color = "blue"
|
|
92
|
-
else: # down
|
|
93
|
-
# Draw down arrow in the middle of the screen
|
|
94
|
-
arrow_y = height // 2
|
|
95
|
-
# Arrow points
|
|
96
|
-
points = [
|
|
97
|
-
(center_x, arrow_y + 50), # Bottom point
|
|
98
|
-
(center_x - arrow_width // 2, arrow_y - 50), # Top left
|
|
99
|
-
(center_x + arrow_width // 2, arrow_y - 50), # Top right
|
|
100
|
-
]
|
|
101
|
-
color = "green"
|
|
102
|
-
|
|
103
|
-
# Draw filled arrow
|
|
104
|
-
draw.polygon(points, fill=color)
|
|
105
|
-
|
|
106
|
-
# Add text showing number of clicks
|
|
107
|
-
text_y = arrow_y + 70 if direction.lower() == "down" else arrow_y - 70
|
|
108
|
-
draw.text((center_x - 40, text_y), f"{clicks} clicks", fill="black")
|
|
109
|
-
|
|
110
|
-
return img
|
|
111
|
-
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.error(f"Error visualizing scroll: {str(e)}")
|
|
114
|
-
# Return a blank image in case of error
|
|
115
|
-
return Image.new("RGB", (800, 600), color="white")
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def calculate_element_center(box: Tuple[int, int, int, int]) -> Tuple[int, int]:
|
|
119
|
-
"""Calculate the center coordinates of a bounding box.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
box: Tuple of (left, top, right, bottom) coordinates
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Tuple of (center_x, center_y) coordinates
|
|
126
|
-
"""
|
|
127
|
-
left, top, right, bottom = box
|
|
128
|
-
center_x = (left + right) // 2
|
|
129
|
-
center_y = (top + bottom) // 2
|
|
130
|
-
return center_x, center_y
|
agent/types/__init__.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Type definitions for the agent package."""
|
|
2
|
-
|
|
3
|
-
from .base import HostConfig, TaskResult, Annotation
|
|
4
|
-
from .messages import Message, Request, Response, StepMessage, DisengageMessage
|
|
5
|
-
from .tools import ToolInvocation, ToolInvocationState, ClientAttachment, ToolResult
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
# Base types
|
|
9
|
-
"HostConfig",
|
|
10
|
-
"TaskResult",
|
|
11
|
-
"Annotation",
|
|
12
|
-
# Message types
|
|
13
|
-
"Message",
|
|
14
|
-
"Request",
|
|
15
|
-
"Response",
|
|
16
|
-
"StepMessage",
|
|
17
|
-
"DisengageMessage",
|
|
18
|
-
# Tool types
|
|
19
|
-
"ToolInvocation",
|
|
20
|
-
"ToolInvocationState",
|
|
21
|
-
"ClientAttachment",
|
|
22
|
-
"ToolResult",
|
|
23
|
-
]
|