cua-agent 0.1.1__py3-none-any.whl → 0.1.3__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 +15 -3
- agent/core/README.md +2 -2
- agent/core/agent.py +105 -176
- agent/core/base_agent.py +1 -1
- agent/core/experiment.py +11 -1
- agent/core/loop.py +1 -1
- agent/core/telemetry.py +138 -0
- agent/providers/anthropic/__init__.py +2 -2
- agent/providers/anthropic/api/client.py +43 -46
- agent/providers/anthropic/loop.py +2 -2
- agent/providers/anthropic/types.py +5 -5
- agent/providers/omni/__init__.py +2 -2
- agent/providers/omni/loop.py +14 -14
- agent/providers/omni/parser.py +1 -1
- agent/providers/omni/prompts.py +0 -14
- agent/providers/omni/types.py +3 -10
- agent/telemetry.py +21 -0
- agent/types/base.py +2 -1
- {cua_agent-0.1.1.dist-info → cua_agent-0.1.3.dist-info}/METADATA +2 -1
- {cua_agent-0.1.1.dist-info → cua_agent-0.1.3.dist-info}/RECORD +22 -20
- {cua_agent-0.1.1.dist-info → cua_agent-0.1.3.dist-info}/WHEEL +0 -0
- {cua_agent-0.1.1.dist-info → cua_agent-0.1.3.dist-info}/entry_points.txt +0 -0
agent/__init__.py
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
__version__ = "0.1.0"
|
|
4
4
|
|
|
5
|
+
# Initialize telemetry when the package is imported
|
|
6
|
+
try:
|
|
7
|
+
from core.telemetry import enable_telemetry, set_dimension
|
|
8
|
+
|
|
9
|
+
# Enable telemetry by default
|
|
10
|
+
enable_telemetry()
|
|
11
|
+
# Set the package version as a dimension
|
|
12
|
+
set_dimension("agent_version", __version__)
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Core telemetry not available
|
|
15
|
+
pass
|
|
16
|
+
|
|
5
17
|
from .core.factory import AgentFactory
|
|
6
18
|
from .core.agent import ComputerAgent
|
|
7
|
-
from .types
|
|
8
|
-
from .
|
|
19
|
+
from .providers.omni.types import LLMProvider, LLM
|
|
20
|
+
from .types.base import Provider, AgentLoop
|
|
9
21
|
|
|
10
|
-
__all__ = ["AgentFactory", "Provider", "ComputerAgent", "
|
|
22
|
+
__all__ = ["AgentFactory", "Provider", "ComputerAgent", "AgentLoop", "LLMProvider", "LLM"]
|
agent/core/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Here's how to use the unified ComputerAgent:
|
|
|
34
34
|
```python
|
|
35
35
|
from agent.core.agent import ComputerAgent
|
|
36
36
|
from agent.types.base import AgenticLoop
|
|
37
|
-
from agent.providers.omni.types import
|
|
37
|
+
from agent.providers.omni.types import LLMProvider
|
|
38
38
|
from computer import Computer
|
|
39
39
|
|
|
40
40
|
# Create a Computer instance
|
|
@@ -44,7 +44,7 @@ computer = Computer()
|
|
|
44
44
|
agent = ComputerAgent(
|
|
45
45
|
computer=computer,
|
|
46
46
|
loop_type=AgenticLoop.OMNI,
|
|
47
|
-
provider=
|
|
47
|
+
provider=LLMProvider.OPENAI,
|
|
48
48
|
model="gpt-4o",
|
|
49
49
|
api_key="your_api_key_here", # Can also use OPENAI_API_KEY environment variable
|
|
50
50
|
save_trajectory=True,
|
agent/core/agent.py
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import logging
|
|
5
5
|
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
6
8
|
from typing import Any, AsyncGenerator, Dict, List, Optional, TYPE_CHECKING, Union, cast
|
|
7
9
|
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
8
11
|
|
|
9
12
|
from computer import Computer
|
|
10
13
|
|
|
11
|
-
from ..types.base import Provider,
|
|
14
|
+
from ..types.base import Provider, AgentLoop
|
|
12
15
|
from .base_agent import BaseComputerAgent
|
|
16
|
+
from ..core.telemetry import record_agent_initialization
|
|
13
17
|
|
|
14
18
|
# Only import types for type checking to avoid circular imports
|
|
15
19
|
if TYPE_CHECKING:
|
|
@@ -18,7 +22,7 @@ if TYPE_CHECKING:
|
|
|
18
22
|
from ..providers.omni.parser import OmniParser
|
|
19
23
|
|
|
20
24
|
# Import the provider types
|
|
21
|
-
from ..providers.omni.types import LLMProvider, LLM, Model, LLMModel
|
|
25
|
+
from ..providers.omni.types import LLMProvider, LLM, Model, LLMModel
|
|
22
26
|
|
|
23
27
|
logger = logging.getLogger(__name__)
|
|
24
28
|
|
|
@@ -26,13 +30,11 @@ logger = logging.getLogger(__name__)
|
|
|
26
30
|
DEFAULT_MODELS = {
|
|
27
31
|
LLMProvider.OPENAI: "gpt-4o",
|
|
28
32
|
LLMProvider.ANTHROPIC: "claude-3-7-sonnet-20250219",
|
|
29
|
-
LLMProvider.GROQ: "llama3-70b-8192",
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
# Map providers to their environment variable names
|
|
33
36
|
ENV_VARS = {
|
|
34
37
|
LLMProvider.OPENAI: "OPENAI_API_KEY",
|
|
35
|
-
LLMProvider.GROQ: "GROQ_API_KEY",
|
|
36
38
|
LLMProvider.ANTHROPIC: "ANTHROPIC_API_KEY",
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -47,7 +49,7 @@ class ComputerAgent(BaseComputerAgent):
|
|
|
47
49
|
def __init__(
|
|
48
50
|
self,
|
|
49
51
|
computer: Computer,
|
|
50
|
-
|
|
52
|
+
loop: AgentLoop = AgentLoop.OMNI,
|
|
51
53
|
model: Optional[Union[LLM, Dict[str, str], str]] = None,
|
|
52
54
|
api_key: Optional[str] = None,
|
|
53
55
|
save_trajectory: bool = True,
|
|
@@ -55,88 +57,77 @@ class ComputerAgent(BaseComputerAgent):
|
|
|
55
57
|
only_n_most_recent_images: Optional[int] = None,
|
|
56
58
|
max_retries: int = 3,
|
|
57
59
|
verbosity: int = logging.INFO,
|
|
60
|
+
telemetry_enabled: bool = True,
|
|
58
61
|
**kwargs,
|
|
59
62
|
):
|
|
60
|
-
"""Initialize
|
|
63
|
+
"""Initialize a ComputerAgent instance.
|
|
61
64
|
|
|
62
65
|
Args:
|
|
63
|
-
computer: Computer instance to control
|
|
64
|
-
|
|
65
|
-
model:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
verbosity: Logging level (standard Python logging levels: logging.DEBUG, logging.INFO, etc.)
|
|
76
|
-
**kwargs: Additional keyword arguments to pass to the loop
|
|
66
|
+
computer: The Computer instance to control
|
|
67
|
+
loop: The agent loop to use: ANTHROPIC or OMNI
|
|
68
|
+
model: The model to use. Can be a string, dict or LLM object.
|
|
69
|
+
Defaults to LLM for the loop type.
|
|
70
|
+
api_key: The API key to use. If None, will use environment variables.
|
|
71
|
+
save_trajectory: Whether to save the trajectory.
|
|
72
|
+
trajectory_dir: The directory to save trajectories to.
|
|
73
|
+
only_n_most_recent_images: Only keep this many most recent images.
|
|
74
|
+
max_retries: Maximum number of retries for failed requests.
|
|
75
|
+
verbosity: Logging level (standard Python logging levels).
|
|
76
|
+
telemetry_enabled: Whether to enable telemetry tracking. Defaults to True.
|
|
77
|
+
**kwargs: Additional keyword arguments to pass to the loop.
|
|
77
78
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
super().__init__(computer)
|
|
80
|
+
self._configure_logging(verbosity)
|
|
81
|
+
logger.info(f"Initializing ComputerAgent with {loop} loop")
|
|
82
|
+
|
|
83
|
+
# Store telemetry preference
|
|
84
|
+
self.telemetry_enabled = telemetry_enabled
|
|
85
|
+
|
|
86
|
+
# Pass telemetry preference to computer if available
|
|
87
|
+
if hasattr(computer, "telemetry_enabled"):
|
|
88
|
+
# Computer doesn't have a setter for telemetry_enabled
|
|
89
|
+
# Use disable_telemetry() method if telemetry is disabled
|
|
90
|
+
if not telemetry_enabled and hasattr(computer, "disable_telemetry"):
|
|
91
|
+
computer.disable_telemetry()
|
|
92
|
+
|
|
93
|
+
# Process the model configuration
|
|
94
|
+
self.model = self._process_model_config(model, loop)
|
|
95
|
+
self.loop_type = loop
|
|
96
|
+
self.api_key = api_key
|
|
97
|
+
|
|
98
|
+
# Store computer
|
|
99
|
+
self.computer = computer
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
# Save trajectory settings
|
|
93
102
|
self.save_trajectory = save_trajectory
|
|
94
103
|
self.trajectory_dir = trajectory_dir
|
|
95
104
|
self.only_n_most_recent_images = only_n_most_recent_images
|
|
96
|
-
self.verbosity = verbosity
|
|
97
|
-
self._kwargs = kwargs # Keep this for loop initialization
|
|
98
105
|
|
|
99
|
-
#
|
|
100
|
-
self.
|
|
106
|
+
# Store the max retries setting
|
|
107
|
+
self.max_retries = max_retries
|
|
101
108
|
|
|
102
|
-
#
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
env_var = (
|
|
108
|
-
ENV_VARS.get(self.model_config.provider)
|
|
109
|
-
if loop_type == AgenticLoop.OMNI
|
|
110
|
-
else "ANTHROPIC_API_KEY"
|
|
111
|
-
)
|
|
112
|
-
if not env_var:
|
|
113
|
-
raise ValueError(
|
|
114
|
-
f"Unsupported provider: {self.model_config.provider}. Please use one of: {list(ENV_VARS.keys())}"
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
api_key = os.environ.get(env_var)
|
|
118
|
-
if not api_key:
|
|
119
|
-
raise ValueError(
|
|
120
|
-
f"No API key provided and {env_var} environment variable is not set.\n"
|
|
121
|
-
f"Please set the {env_var} environment variable or pass the api_key directly:\n"
|
|
122
|
-
f" - Export in terminal: export {env_var}=your_api_key_here\n"
|
|
123
|
-
f" - Add to .env file: {env_var}=your_api_key_here\n"
|
|
124
|
-
f" - Pass directly: api_key='your_api_key_here'"
|
|
125
|
-
)
|
|
126
|
-
self.api_key = api_key
|
|
109
|
+
# Initialize message history
|
|
110
|
+
self.messages = []
|
|
111
|
+
|
|
112
|
+
# Extra kwargs for the loop
|
|
113
|
+
self.loop_kwargs = kwargs
|
|
127
114
|
|
|
128
|
-
# Initialize the
|
|
115
|
+
# Initialize the actual loop implementation
|
|
129
116
|
self.loop = self._init_loop()
|
|
130
|
-
|
|
117
|
+
|
|
118
|
+
# Record initialization in telemetry if enabled
|
|
119
|
+
if telemetry_enabled:
|
|
120
|
+
record_agent_initialization()
|
|
121
|
+
|
|
131
122
|
def _process_model_config(
|
|
132
|
-
self, model_input: Optional[Union[LLM, Dict[str, str], str]],
|
|
123
|
+
self, model_input: Optional[Union[LLM, Dict[str, str], str]], loop: AgentLoop
|
|
133
124
|
) -> LLM:
|
|
134
125
|
"""Process and normalize model configuration.
|
|
135
|
-
|
|
126
|
+
|
|
136
127
|
Args:
|
|
137
128
|
model_input: Input model configuration (LLM, dict, string, or None)
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
loop: The loop type being used
|
|
130
|
+
|
|
140
131
|
Returns:
|
|
141
132
|
Normalized LLM instance
|
|
142
133
|
"""
|
|
@@ -144,31 +135,28 @@ class ComputerAgent(BaseComputerAgent):
|
|
|
144
135
|
if model_input is None:
|
|
145
136
|
# Use Anthropic for Anthropic loop, OpenAI for Omni loop
|
|
146
137
|
default_provider = (
|
|
147
|
-
LLMProvider.ANTHROPIC if
|
|
138
|
+
LLMProvider.ANTHROPIC if loop == AgentLoop.ANTHROPIC else LLMProvider.OPENAI
|
|
148
139
|
)
|
|
149
140
|
return LLM(provider=default_provider)
|
|
150
|
-
|
|
141
|
+
|
|
151
142
|
# Handle case where model_input is already a LLM or one of its aliases
|
|
152
143
|
if isinstance(model_input, (LLM, Model, LLMModel)):
|
|
153
144
|
return model_input
|
|
154
|
-
|
|
145
|
+
|
|
155
146
|
# Handle case where model_input is a dict
|
|
156
147
|
if isinstance(model_input, dict):
|
|
157
148
|
provider = model_input.get("provider", LLMProvider.OPENAI)
|
|
158
149
|
if isinstance(provider, str):
|
|
159
150
|
provider = LLMProvider(provider)
|
|
160
|
-
return LLM(
|
|
161
|
-
|
|
162
|
-
name=model_input.get("name")
|
|
163
|
-
)
|
|
164
|
-
|
|
151
|
+
return LLM(provider=provider, name=model_input.get("name"))
|
|
152
|
+
|
|
165
153
|
# Handle case where model_input is a string (model name)
|
|
166
154
|
if isinstance(model_input, str):
|
|
167
155
|
default_provider = (
|
|
168
|
-
LLMProvider.ANTHROPIC if
|
|
156
|
+
LLMProvider.ANTHROPIC if loop == AgentLoop.ANTHROPIC else LLMProvider.OPENAI
|
|
169
157
|
)
|
|
170
158
|
return LLM(provider=default_provider, name=model_input)
|
|
171
|
-
|
|
159
|
+
|
|
172
160
|
raise ValueError(f"Unsupported model configuration: {model_input}")
|
|
173
161
|
|
|
174
162
|
def _configure_logging(self, verbosity: int):
|
|
@@ -199,12 +187,12 @@ class ComputerAgent(BaseComputerAgent):
|
|
|
199
187
|
from ..providers.omni.loop import OmniLoop
|
|
200
188
|
from ..providers.omni.parser import OmniParser
|
|
201
189
|
|
|
202
|
-
if self.loop_type ==
|
|
190
|
+
if self.loop_type == AgentLoop.ANTHROPIC:
|
|
203
191
|
from ..providers.anthropic.loop import AnthropicLoop
|
|
204
192
|
|
|
205
193
|
# Ensure we always have a valid model name
|
|
206
|
-
model_name = self.
|
|
207
|
-
|
|
194
|
+
model_name = self.model.name or DEFAULT_MODELS[LLMProvider.ANTHROPIC]
|
|
195
|
+
|
|
208
196
|
return AnthropicLoop(
|
|
209
197
|
api_key=self.api_key,
|
|
210
198
|
model=model_name,
|
|
@@ -212,119 +200,60 @@ class ComputerAgent(BaseComputerAgent):
|
|
|
212
200
|
save_trajectory=self.save_trajectory,
|
|
213
201
|
base_dir=self.trajectory_dir,
|
|
214
202
|
only_n_most_recent_images=self.only_n_most_recent_images,
|
|
215
|
-
**self.
|
|
203
|
+
**self.loop_kwargs,
|
|
216
204
|
)
|
|
217
205
|
|
|
218
206
|
# Initialize parser for OmniLoop with appropriate device
|
|
219
|
-
if "parser" not in self.
|
|
220
|
-
self.
|
|
207
|
+
if "parser" not in self.loop_kwargs:
|
|
208
|
+
self.loop_kwargs["parser"] = OmniParser()
|
|
221
209
|
|
|
222
210
|
# Ensure we always have a valid model name
|
|
223
|
-
model_name = self.
|
|
224
|
-
|
|
211
|
+
model_name = self.model.name or DEFAULT_MODELS[self.model.provider]
|
|
212
|
+
|
|
225
213
|
return OmniLoop(
|
|
226
|
-
provider=self.
|
|
214
|
+
provider=self.model.provider,
|
|
227
215
|
api_key=self.api_key,
|
|
228
216
|
model=model_name,
|
|
229
217
|
computer=self.computer,
|
|
230
218
|
save_trajectory=self.save_trajectory,
|
|
231
219
|
base_dir=self.trajectory_dir,
|
|
232
220
|
only_n_most_recent_images=self.only_n_most_recent_images,
|
|
233
|
-
**self.
|
|
221
|
+
**self.loop_kwargs,
|
|
234
222
|
)
|
|
235
223
|
|
|
236
224
|
async def _execute_task(self, task: str) -> AsyncGenerator[Dict[str, Any], None]:
|
|
237
|
-
"""Execute a task using the appropriate loop.
|
|
225
|
+
"""Execute a task using the appropriate agent loop.
|
|
238
226
|
|
|
239
227
|
Args:
|
|
240
|
-
task:
|
|
228
|
+
task: The task to execute
|
|
241
229
|
|
|
242
|
-
|
|
243
|
-
|
|
230
|
+
Returns:
|
|
231
|
+
AsyncGenerator yielding task outputs
|
|
244
232
|
"""
|
|
233
|
+
logger.info(f"Executing task: {task}")
|
|
234
|
+
|
|
245
235
|
try:
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
messages = [{"role": "user", "content": [{"type": "text", "text": task}]}]
|
|
250
|
-
else:
|
|
251
|
-
# Cua format
|
|
252
|
-
messages = [{"role": "user", "content": task}]
|
|
253
|
-
|
|
254
|
-
# Run the loop
|
|
255
|
-
try:
|
|
256
|
-
async for result in self.loop.run(messages):
|
|
257
|
-
if result is None:
|
|
258
|
-
break
|
|
259
|
-
|
|
260
|
-
# Handle error case
|
|
261
|
-
if "error" in result:
|
|
262
|
-
yield {
|
|
263
|
-
"role": "assistant",
|
|
264
|
-
"content": result["error"],
|
|
265
|
-
"metadata": {"title": "❌ Error"},
|
|
266
|
-
}
|
|
267
|
-
continue
|
|
268
|
-
|
|
269
|
-
# Extract content and metadata based on loop type
|
|
270
|
-
if self.loop_type == AgenticLoop.ANTHROPIC:
|
|
271
|
-
# Handle Anthropic format
|
|
272
|
-
if "content" in result:
|
|
273
|
-
content_text = ""
|
|
274
|
-
for content_block in result["content"]:
|
|
275
|
-
try:
|
|
276
|
-
# Try to access the text attribute directly
|
|
277
|
-
content_text += content_block.text
|
|
278
|
-
except (AttributeError, TypeError):
|
|
279
|
-
# If it's a dictionary instead of an object
|
|
280
|
-
if isinstance(content_block, dict) and "text" in content_block:
|
|
281
|
-
content_text += content_block["text"]
|
|
282
|
-
|
|
283
|
-
yield {
|
|
284
|
-
"role": "assistant",
|
|
285
|
-
"content": content_text,
|
|
286
|
-
"metadata": result.get("parsed_screen", {}),
|
|
287
|
-
}
|
|
288
|
-
else:
|
|
289
|
-
yield {
|
|
290
|
-
"role": "assistant",
|
|
291
|
-
"content": str(result),
|
|
292
|
-
"metadata": {"title": "Screen Analysis"},
|
|
293
|
-
}
|
|
294
|
-
else:
|
|
295
|
-
# Handle Omni format
|
|
296
|
-
content = ""
|
|
297
|
-
metadata = {"title": "Screen Analysis"}
|
|
298
|
-
|
|
299
|
-
# If result has content (normal case)
|
|
300
|
-
if "content" in result:
|
|
301
|
-
content = result["content"]
|
|
302
|
-
|
|
303
|
-
# Ensure metadata has a title
|
|
304
|
-
if isinstance(content, dict) and "metadata" in content:
|
|
305
|
-
metadata = content["metadata"]
|
|
306
|
-
if "title" not in metadata:
|
|
307
|
-
metadata["title"] = "Screen Analysis"
|
|
308
|
-
|
|
309
|
-
# For string content, convert to proper format
|
|
310
|
-
if isinstance(content, str):
|
|
311
|
-
content = content
|
|
312
|
-
elif isinstance(content, dict) and "content" in content:
|
|
313
|
-
content = content.get("content", "")
|
|
314
|
-
|
|
315
|
-
yield {"role": "assistant", "content": content, "metadata": metadata}
|
|
316
|
-
except Exception as e:
|
|
317
|
-
logger.error(f"Error running the loop: {str(e)}")
|
|
318
|
-
yield {
|
|
319
|
-
"role": "assistant",
|
|
320
|
-
"content": f"Error running the agent loop: {str(e)}",
|
|
321
|
-
"metadata": {"title": "❌ Loop Error"},
|
|
322
|
-
}
|
|
236
|
+
# Create a message from the task
|
|
237
|
+
task_message = {"role": "user", "content": task}
|
|
238
|
+
messages_with_task = self.messages + [task_message]
|
|
323
239
|
|
|
240
|
+
# Use the run method of the loop
|
|
241
|
+
async for output in self.loop.run(messages_with_task):
|
|
242
|
+
yield output
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Error executing task: {e}")
|
|
245
|
+
raise
|
|
246
|
+
finally:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
async def _execute_action(self, action_type: str, **action_params) -> Any:
|
|
250
|
+
"""Execute an action with telemetry tracking."""
|
|
251
|
+
try:
|
|
252
|
+
# Execute the action
|
|
253
|
+
result = await super()._execute_action(action_type, **action_params)
|
|
254
|
+
return result
|
|
324
255
|
except Exception as e:
|
|
325
|
-
logger.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"metadata": {"title": "❌ Error"},
|
|
330
|
-
}
|
|
256
|
+
logger.exception(f"Error executing action {action_type}: {e}")
|
|
257
|
+
raise
|
|
258
|
+
finally:
|
|
259
|
+
pass
|
agent/core/base_agent.py
CHANGED
|
@@ -113,7 +113,7 @@ class BaseComputerAgent(ABC):
|
|
|
113
113
|
# Take a test screenshot to verify the computer is working
|
|
114
114
|
logger.info("Testing computer with a screenshot...")
|
|
115
115
|
try:
|
|
116
|
-
test_screenshot = await self.computer.screenshot()
|
|
116
|
+
test_screenshot = await self.computer.interface.screenshot()
|
|
117
117
|
# Determine the screenshot size based on its type
|
|
118
118
|
if isinstance(test_screenshot, bytes):
|
|
119
119
|
size = len(test_screenshot)
|
agent/core/experiment.py
CHANGED
|
@@ -8,6 +8,7 @@ from datetime import datetime
|
|
|
8
8
|
from typing import Any, Dict, List, Optional
|
|
9
9
|
from PIL import Image
|
|
10
10
|
import json
|
|
11
|
+
import re
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -106,9 +107,18 @@ class ExperimentManager:
|
|
|
106
107
|
# Increment screenshot counter
|
|
107
108
|
self.screenshot_count += 1
|
|
108
109
|
|
|
110
|
+
# Sanitize action_type to ensure valid filename
|
|
111
|
+
# Replace characters that are not safe for filenames
|
|
112
|
+
sanitized_action = ""
|
|
113
|
+
if action_type:
|
|
114
|
+
# Replace invalid filename characters with underscores
|
|
115
|
+
sanitized_action = re.sub(r'[\\/*?:"<>|]', "_", action_type)
|
|
116
|
+
# Limit the length to avoid excessively long filenames
|
|
117
|
+
sanitized_action = sanitized_action[:50]
|
|
118
|
+
|
|
109
119
|
# Create a descriptive filename
|
|
110
120
|
timestamp = int(datetime.now().timestamp() * 1000)
|
|
111
|
-
action_suffix = f"_{
|
|
121
|
+
action_suffix = f"_{sanitized_action}" if sanitized_action else ""
|
|
112
122
|
filename = f"screenshot_{self.screenshot_count:03d}{action_suffix}_{timestamp}.png"
|
|
113
123
|
|
|
114
124
|
# Save directly to the turn directory
|
agent/core/loop.py
CHANGED
agent/core/telemetry.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Agent telemetry for tracking anonymous usage and feature usage."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
|
|
10
|
+
# Import the core telemetry module
|
|
11
|
+
TELEMETRY_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from core.telemetry import (
|
|
15
|
+
record_event,
|
|
16
|
+
increment,
|
|
17
|
+
get_telemetry_client,
|
|
18
|
+
flush,
|
|
19
|
+
is_telemetry_enabled,
|
|
20
|
+
is_telemetry_globally_disabled,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def increment_counter(counter_name: str, value: int = 1) -> None:
|
|
24
|
+
"""Wrapper for increment to maintain backward compatibility."""
|
|
25
|
+
if is_telemetry_enabled():
|
|
26
|
+
increment(counter_name, value)
|
|
27
|
+
|
|
28
|
+
def set_dimension(name: str, value: Any) -> None:
|
|
29
|
+
"""Set a dimension that will be attached to all events."""
|
|
30
|
+
logger = logging.getLogger("cua.agent.telemetry")
|
|
31
|
+
logger.debug(f"Setting dimension {name}={value}")
|
|
32
|
+
|
|
33
|
+
TELEMETRY_AVAILABLE = True
|
|
34
|
+
logger = logging.getLogger("cua.agent.telemetry")
|
|
35
|
+
logger.info("Successfully imported telemetry")
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
logger = logging.getLogger("cua.agent.telemetry")
|
|
38
|
+
logger.warning(f"Could not import telemetry: {e}")
|
|
39
|
+
TELEMETRY_AVAILABLE = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Local fallbacks in case core telemetry isn't available
|
|
43
|
+
def _noop(*args: Any, **kwargs: Any) -> None:
|
|
44
|
+
"""No-op function for when telemetry is not available."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger("cua.agent.telemetry")
|
|
49
|
+
|
|
50
|
+
# If telemetry isn't available, use no-op functions
|
|
51
|
+
if not TELEMETRY_AVAILABLE:
|
|
52
|
+
logger.debug("Telemetry not available, using no-op functions")
|
|
53
|
+
record_event = _noop # type: ignore
|
|
54
|
+
increment_counter = _noop # type: ignore
|
|
55
|
+
set_dimension = _noop # type: ignore
|
|
56
|
+
get_telemetry_client = lambda: None # type: ignore
|
|
57
|
+
flush = _noop # type: ignore
|
|
58
|
+
is_telemetry_enabled = lambda: False # type: ignore
|
|
59
|
+
is_telemetry_globally_disabled = lambda: True # type: ignore
|
|
60
|
+
|
|
61
|
+
# Get system info once to use in telemetry
|
|
62
|
+
SYSTEM_INFO = {
|
|
63
|
+
"os": platform.system().lower(),
|
|
64
|
+
"os_version": platform.release(),
|
|
65
|
+
"python_version": platform.python_version(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def enable_telemetry() -> bool:
|
|
70
|
+
"""Enable telemetry if available.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bool: True if telemetry was successfully enabled, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
global TELEMETRY_AVAILABLE
|
|
76
|
+
|
|
77
|
+
# Check if globally disabled using core function
|
|
78
|
+
if TELEMETRY_AVAILABLE and is_telemetry_globally_disabled():
|
|
79
|
+
logger.info("Telemetry is globally disabled via environment variable - cannot enable")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
# Already enabled
|
|
83
|
+
if TELEMETRY_AVAILABLE:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
# Try to import and enable
|
|
87
|
+
try:
|
|
88
|
+
from core.telemetry import (
|
|
89
|
+
record_event,
|
|
90
|
+
increment,
|
|
91
|
+
get_telemetry_client,
|
|
92
|
+
flush,
|
|
93
|
+
is_telemetry_globally_disabled,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Check again after import
|
|
97
|
+
if is_telemetry_globally_disabled():
|
|
98
|
+
logger.info("Telemetry is globally disabled via environment variable - cannot enable")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
TELEMETRY_AVAILABLE = True
|
|
102
|
+
logger.info("Telemetry successfully enabled")
|
|
103
|
+
return True
|
|
104
|
+
except ImportError as e:
|
|
105
|
+
logger.warning(f"Could not enable telemetry: {e}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def disable_telemetry() -> None:
|
|
110
|
+
"""Disable telemetry for this session."""
|
|
111
|
+
global TELEMETRY_AVAILABLE
|
|
112
|
+
TELEMETRY_AVAILABLE = False
|
|
113
|
+
logger.info("Telemetry disabled for this session")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_telemetry_enabled() -> bool:
|
|
117
|
+
"""Check if telemetry is enabled.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
bool: True if telemetry is enabled, False otherwise
|
|
121
|
+
"""
|
|
122
|
+
# Use the core function if available, otherwise use our local flag
|
|
123
|
+
if TELEMETRY_AVAILABLE:
|
|
124
|
+
from core.telemetry import is_telemetry_enabled as core_is_enabled
|
|
125
|
+
|
|
126
|
+
return core_is_enabled()
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def record_agent_initialization() -> None:
|
|
131
|
+
"""Record when an agent instance is initialized."""
|
|
132
|
+
if TELEMETRY_AVAILABLE and is_telemetry_enabled():
|
|
133
|
+
record_event("agent_initialized", SYSTEM_INFO)
|
|
134
|
+
|
|
135
|
+
# Set dimensions that will be attached to all events
|
|
136
|
+
set_dimension("os", SYSTEM_INFO["os"])
|
|
137
|
+
set_dimension("os_version", SYSTEM_INFO["os_version"])
|
|
138
|
+
set_dimension("python_version", SYSTEM_INFO["python_version"])
|
|
@@ -3,25 +3,28 @@ import httpx
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from anthropic import Anthropic, AnthropicBedrock, AnthropicVertex
|
|
5
5
|
from anthropic.types.beta import BetaMessage, BetaMessageParam, BetaToolUnionParam
|
|
6
|
-
from ..types import
|
|
6
|
+
from ..types import LLMProvider
|
|
7
7
|
from .logging import log_api_interaction
|
|
8
8
|
import random
|
|
9
9
|
import logging
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
class APIConnectionError(Exception):
|
|
14
15
|
"""Error raised when there are connection issues with the API."""
|
|
16
|
+
|
|
15
17
|
pass
|
|
16
18
|
|
|
19
|
+
|
|
17
20
|
class BaseAnthropicClient:
|
|
18
21
|
"""Base class for Anthropic API clients."""
|
|
19
|
-
|
|
22
|
+
|
|
20
23
|
MAX_RETRIES = 10
|
|
21
24
|
INITIAL_RETRY_DELAY = 1.0
|
|
22
25
|
MAX_RETRY_DELAY = 60.0
|
|
23
26
|
JITTER_FACTOR = 0.1
|
|
24
|
-
|
|
27
|
+
|
|
25
28
|
async def create_message(
|
|
26
29
|
self,
|
|
27
30
|
*,
|
|
@@ -36,79 +39,67 @@ class BaseAnthropicClient:
|
|
|
36
39
|
|
|
37
40
|
async def _make_api_call_with_retries(self, api_call):
|
|
38
41
|
"""Make an API call with exponential backoff retry logic.
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
Args:
|
|
41
44
|
api_call: Async function that makes the actual API call
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
Returns:
|
|
44
47
|
API response
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
Raises:
|
|
47
50
|
APIConnectionError: If all retries fail
|
|
48
51
|
"""
|
|
49
52
|
retry_count = 0
|
|
50
53
|
last_error = None
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
while retry_count < self.MAX_RETRIES:
|
|
53
56
|
try:
|
|
54
57
|
return await api_call()
|
|
55
58
|
except Exception as e:
|
|
56
59
|
last_error = e
|
|
57
60
|
retry_count += 1
|
|
58
|
-
|
|
61
|
+
|
|
59
62
|
if retry_count == self.MAX_RETRIES:
|
|
60
63
|
break
|
|
61
|
-
|
|
64
|
+
|
|
62
65
|
# Calculate delay with exponential backoff and jitter
|
|
63
66
|
delay = min(
|
|
64
|
-
self.INITIAL_RETRY_DELAY * (2 ** (retry_count - 1)),
|
|
65
|
-
self.MAX_RETRY_DELAY
|
|
67
|
+
self.INITIAL_RETRY_DELAY * (2 ** (retry_count - 1)), self.MAX_RETRY_DELAY
|
|
66
68
|
)
|
|
67
69
|
# Add jitter to avoid thundering herd
|
|
68
70
|
jitter = delay * self.JITTER_FACTOR * (2 * random.random() - 1)
|
|
69
71
|
final_delay = delay + jitter
|
|
70
|
-
|
|
72
|
+
|
|
71
73
|
logger.info(
|
|
72
74
|
f"Retrying request (attempt {retry_count}/{self.MAX_RETRIES}) "
|
|
73
75
|
f"in {final_delay:.2f} seconds after error: {str(e)}"
|
|
74
76
|
)
|
|
75
77
|
await asyncio.sleep(final_delay)
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
raise APIConnectionError(
|
|
78
|
-
f"Failed after {self.MAX_RETRIES} retries. "
|
|
79
|
-
f"Last error: {str(last_error)}"
|
|
80
|
+
f"Failed after {self.MAX_RETRIES} retries. " f"Last error: {str(last_error)}"
|
|
80
81
|
)
|
|
81
82
|
|
|
83
|
+
|
|
82
84
|
class AnthropicDirectClient(BaseAnthropicClient):
|
|
83
85
|
"""Direct Anthropic API client implementation."""
|
|
84
|
-
|
|
86
|
+
|
|
85
87
|
def __init__(self, api_key: str, model: str):
|
|
86
88
|
self.model = model
|
|
87
|
-
self.client = Anthropic(
|
|
88
|
-
|
|
89
|
-
http_client=self._create_http_client()
|
|
90
|
-
)
|
|
91
|
-
|
|
89
|
+
self.client = Anthropic(api_key=api_key, http_client=self._create_http_client())
|
|
90
|
+
|
|
92
91
|
def _create_http_client(self) -> httpx.Client:
|
|
93
92
|
"""Create an HTTP client with appropriate settings."""
|
|
94
93
|
return httpx.Client(
|
|
95
94
|
verify=True,
|
|
96
|
-
timeout=httpx.Timeout(
|
|
97
|
-
connect=30.0,
|
|
98
|
-
read=300.0,
|
|
99
|
-
write=30.0,
|
|
100
|
-
pool=30.0
|
|
101
|
-
),
|
|
95
|
+
timeout=httpx.Timeout(connect=30.0, read=300.0, write=30.0, pool=30.0),
|
|
102
96
|
transport=httpx.HTTPTransport(
|
|
103
97
|
retries=3,
|
|
104
98
|
verify=True,
|
|
105
|
-
limits=httpx.Limits(
|
|
106
|
-
|
|
107
|
-
max_connections=10
|
|
108
|
-
)
|
|
109
|
-
)
|
|
99
|
+
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
|
100
|
+
),
|
|
110
101
|
)
|
|
111
|
-
|
|
102
|
+
|
|
112
103
|
async def create_message(
|
|
113
104
|
self,
|
|
114
105
|
*,
|
|
@@ -119,6 +110,7 @@ class AnthropicDirectClient(BaseAnthropicClient):
|
|
|
119
110
|
betas: list[str],
|
|
120
111
|
) -> BetaMessage:
|
|
121
112
|
"""Create a message using the direct Anthropic API with retry logic."""
|
|
113
|
+
|
|
122
114
|
async def api_call():
|
|
123
115
|
response = self.client.beta.messages.with_raw_response.create(
|
|
124
116
|
max_tokens=max_tokens,
|
|
@@ -130,20 +122,21 @@ class AnthropicDirectClient(BaseAnthropicClient):
|
|
|
130
122
|
)
|
|
131
123
|
log_api_interaction(response.http_response.request, response.http_response, None)
|
|
132
124
|
return response.parse()
|
|
133
|
-
|
|
125
|
+
|
|
134
126
|
try:
|
|
135
127
|
return await self._make_api_call_with_retries(api_call)
|
|
136
128
|
except Exception as e:
|
|
137
129
|
log_api_interaction(None, None, e)
|
|
138
130
|
raise
|
|
139
131
|
|
|
132
|
+
|
|
140
133
|
class AnthropicVertexClient(BaseAnthropicClient):
|
|
141
134
|
"""Google Cloud Vertex AI implementation of Anthropic client."""
|
|
142
|
-
|
|
135
|
+
|
|
143
136
|
def __init__(self, model: str):
|
|
144
137
|
self.model = model
|
|
145
138
|
self.client = AnthropicVertex()
|
|
146
|
-
|
|
139
|
+
|
|
147
140
|
async def create_message(
|
|
148
141
|
self,
|
|
149
142
|
*,
|
|
@@ -154,6 +147,7 @@ class AnthropicVertexClient(BaseAnthropicClient):
|
|
|
154
147
|
betas: list[str],
|
|
155
148
|
) -> BetaMessage:
|
|
156
149
|
"""Create a message using Vertex AI with retry logic."""
|
|
150
|
+
|
|
157
151
|
async def api_call():
|
|
158
152
|
response = self.client.beta.messages.with_raw_response.create(
|
|
159
153
|
max_tokens=max_tokens,
|
|
@@ -165,20 +159,21 @@ class AnthropicVertexClient(BaseAnthropicClient):
|
|
|
165
159
|
)
|
|
166
160
|
log_api_interaction(response.http_response.request, response.http_response, None)
|
|
167
161
|
return response.parse()
|
|
168
|
-
|
|
162
|
+
|
|
169
163
|
try:
|
|
170
164
|
return await self._make_api_call_with_retries(api_call)
|
|
171
165
|
except Exception as e:
|
|
172
166
|
log_api_interaction(None, None, e)
|
|
173
167
|
raise
|
|
174
168
|
|
|
169
|
+
|
|
175
170
|
class AnthropicBedrockClient(BaseAnthropicClient):
|
|
176
171
|
"""AWS Bedrock implementation of Anthropic client."""
|
|
177
|
-
|
|
172
|
+
|
|
178
173
|
def __init__(self, model: str):
|
|
179
174
|
self.model = model
|
|
180
175
|
self.client = AnthropicBedrock()
|
|
181
|
-
|
|
176
|
+
|
|
182
177
|
async def create_message(
|
|
183
178
|
self,
|
|
184
179
|
*,
|
|
@@ -189,6 +184,7 @@ class AnthropicBedrockClient(BaseAnthropicClient):
|
|
|
189
184
|
betas: list[str],
|
|
190
185
|
) -> BetaMessage:
|
|
191
186
|
"""Create a message using AWS Bedrock with retry logic."""
|
|
187
|
+
|
|
192
188
|
async def api_call():
|
|
193
189
|
response = self.client.beta.messages.with_raw_response.create(
|
|
194
190
|
max_tokens=max_tokens,
|
|
@@ -200,23 +196,24 @@ class AnthropicBedrockClient(BaseAnthropicClient):
|
|
|
200
196
|
)
|
|
201
197
|
log_api_interaction(response.http_response.request, response.http_response, None)
|
|
202
198
|
return response.parse()
|
|
203
|
-
|
|
199
|
+
|
|
204
200
|
try:
|
|
205
201
|
return await self._make_api_call_with_retries(api_call)
|
|
206
202
|
except Exception as e:
|
|
207
203
|
log_api_interaction(None, None, e)
|
|
208
204
|
raise
|
|
209
205
|
|
|
206
|
+
|
|
210
207
|
class AnthropicClientFactory:
|
|
211
208
|
"""Factory for creating appropriate Anthropic client implementations."""
|
|
212
|
-
|
|
209
|
+
|
|
213
210
|
@staticmethod
|
|
214
|
-
def create_client(provider:
|
|
211
|
+
def create_client(provider: LLMProvider, api_key: str, model: str) -> BaseAnthropicClient:
|
|
215
212
|
"""Create an appropriate client based on the provider."""
|
|
216
|
-
if provider ==
|
|
213
|
+
if provider == LLMProvider.ANTHROPIC:
|
|
217
214
|
return AnthropicDirectClient(api_key, model)
|
|
218
|
-
elif provider ==
|
|
215
|
+
elif provider == LLMProvider.VERTEX:
|
|
219
216
|
return AnthropicVertexClient(model)
|
|
220
|
-
elif provider ==
|
|
217
|
+
elif provider == LLMProvider.BEDROCK:
|
|
221
218
|
return AnthropicBedrockClient(model)
|
|
222
|
-
raise ValueError(f"Unsupported provider: {provider}")
|
|
219
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
@@ -32,7 +32,7 @@ from .tools.manager import ToolManager
|
|
|
32
32
|
from .messages.manager import MessageManager
|
|
33
33
|
from .callbacks.manager import CallbackManager
|
|
34
34
|
from .prompts import SYSTEM_PROMPT
|
|
35
|
-
from .types import
|
|
35
|
+
from .types import LLMProvider
|
|
36
36
|
from .tools import ToolResult
|
|
37
37
|
|
|
38
38
|
# Constants
|
|
@@ -86,7 +86,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
86
86
|
self.model = "claude-3-7-sonnet-20250219"
|
|
87
87
|
|
|
88
88
|
# Anthropic-specific attributes
|
|
89
|
-
self.provider =
|
|
89
|
+
self.provider = LLMProvider.ANTHROPIC
|
|
90
90
|
self.client = None
|
|
91
91
|
self.retry_count = 0
|
|
92
92
|
self.tool_manager = None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from enum import StrEnum
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
4
|
+
class LLMProvider(StrEnum):
|
|
5
5
|
"""Enum for supported API providers."""
|
|
6
6
|
|
|
7
7
|
ANTHROPIC = "anthropic"
|
|
@@ -9,8 +9,8 @@ class APIProvider(StrEnum):
|
|
|
9
9
|
VERTEX = "vertex"
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
PROVIDER_TO_DEFAULT_MODEL_NAME: dict[
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
PROVIDER_TO_DEFAULT_MODEL_NAME: dict[LLMProvider, str] = {
|
|
13
|
+
LLMProvider.ANTHROPIC: "claude-3-7-sonnet-20250219",
|
|
14
|
+
LLMProvider.BEDROCK: "anthropic.claude-3-7-sonnet-20250219-v2:0",
|
|
15
|
+
LLMProvider.VERTEX: "claude-3-5-sonnet-v2@20241022",
|
|
16
16
|
}
|
agent/providers/omni/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# The OmniComputerAgent has been replaced by the unified ComputerAgent
|
|
4
4
|
# which can be found in agent.core.agent
|
|
5
|
-
from .types import
|
|
5
|
+
from .types import LLMProvider
|
|
6
6
|
from .experiment import ExperimentManager
|
|
7
7
|
from .visualization import visualize_click, visualize_scroll, calculate_element_center
|
|
8
8
|
from .image_utils import (
|
|
@@ -14,7 +14,7 @@ from .image_utils import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
|
-
"
|
|
17
|
+
"LLMProvider",
|
|
18
18
|
"ExperimentManager",
|
|
19
19
|
"visualize_click",
|
|
20
20
|
"visualize_scroll",
|
agent/providers/omni/loop.py
CHANGED
|
@@ -17,7 +17,7 @@ import copy
|
|
|
17
17
|
from .parser import OmniParser, ParseResult, ParserMetadata, UIElement
|
|
18
18
|
from ...core.loop import BaseLoop
|
|
19
19
|
from computer import Computer
|
|
20
|
-
from .types import
|
|
20
|
+
from .types import LLMProvider
|
|
21
21
|
from .clients.base import BaseOmniClient
|
|
22
22
|
from .clients.openai import OpenAIClient
|
|
23
23
|
from .clients.groq import GroqClient
|
|
@@ -46,7 +46,7 @@ class OmniLoop(BaseLoop):
|
|
|
46
46
|
def __init__(
|
|
47
47
|
self,
|
|
48
48
|
parser: OmniParser,
|
|
49
|
-
provider:
|
|
49
|
+
provider: LLMProvider,
|
|
50
50
|
api_key: str,
|
|
51
51
|
model: str,
|
|
52
52
|
computer: Computer,
|
|
@@ -180,11 +180,11 @@ class OmniLoop(BaseLoop):
|
|
|
180
180
|
try:
|
|
181
181
|
logger.info(f"Initializing {self.provider} client with model {self.model}...")
|
|
182
182
|
|
|
183
|
-
if self.provider ==
|
|
183
|
+
if self.provider == LLMProvider.OPENAI:
|
|
184
184
|
self.client = OpenAIClient(api_key=self.api_key, model=self.model)
|
|
185
|
-
elif self.provider ==
|
|
185
|
+
elif self.provider == LLMProvider.GROQ:
|
|
186
186
|
self.client = GroqClient(api_key=self.api_key, model=self.model)
|
|
187
|
-
elif self.provider ==
|
|
187
|
+
elif self.provider == LLMProvider.ANTHROPIC:
|
|
188
188
|
self.client = AnthropicClient(
|
|
189
189
|
api_key=self.api_key,
|
|
190
190
|
model=self.model,
|
|
@@ -228,7 +228,7 @@ class OmniLoop(BaseLoop):
|
|
|
228
228
|
prepared_messages = self.message_manager.get_formatted_messages(provider_name)
|
|
229
229
|
|
|
230
230
|
# Filter out system messages for Anthropic
|
|
231
|
-
if self.provider ==
|
|
231
|
+
if self.provider == LLMProvider.ANTHROPIC:
|
|
232
232
|
filtered_messages = [
|
|
233
233
|
msg for msg in prepared_messages if msg["role"] != "system"
|
|
234
234
|
]
|
|
@@ -238,7 +238,7 @@ class OmniLoop(BaseLoop):
|
|
|
238
238
|
# Log request
|
|
239
239
|
request_data = {"messages": filtered_messages, "max_tokens": self.max_tokens}
|
|
240
240
|
|
|
241
|
-
if self.provider ==
|
|
241
|
+
if self.provider == LLMProvider.ANTHROPIC:
|
|
242
242
|
request_data["system"] = self._get_system_prompt()
|
|
243
243
|
else:
|
|
244
244
|
request_data["system"] = system_prompt
|
|
@@ -255,7 +255,7 @@ class OmniLoop(BaseLoop):
|
|
|
255
255
|
|
|
256
256
|
if is_async:
|
|
257
257
|
# For async implementations (AnthropicClient)
|
|
258
|
-
if self.provider ==
|
|
258
|
+
if self.provider == LLMProvider.ANTHROPIC:
|
|
259
259
|
response = await run_method(
|
|
260
260
|
messages=filtered_messages,
|
|
261
261
|
system=self._get_system_prompt(),
|
|
@@ -269,7 +269,7 @@ class OmniLoop(BaseLoop):
|
|
|
269
269
|
)
|
|
270
270
|
else:
|
|
271
271
|
# For non-async implementations (GroqClient, etc.)
|
|
272
|
-
if self.provider ==
|
|
272
|
+
if self.provider == LLMProvider.ANTHROPIC:
|
|
273
273
|
response = run_method(
|
|
274
274
|
messages=filtered_messages,
|
|
275
275
|
system=self._get_system_prompt(),
|
|
@@ -339,7 +339,7 @@ class OmniLoop(BaseLoop):
|
|
|
339
339
|
action_screenshot_saved = False
|
|
340
340
|
try:
|
|
341
341
|
# Handle Anthropic response format
|
|
342
|
-
if self.provider ==
|
|
342
|
+
if self.provider == LLMProvider.ANTHROPIC:
|
|
343
343
|
if hasattr(response, "content") and isinstance(response.content, list):
|
|
344
344
|
# Extract text from content blocks
|
|
345
345
|
for block in response.content:
|
|
@@ -563,7 +563,7 @@ class OmniLoop(BaseLoop):
|
|
|
563
563
|
"""Process and add screen info to messages."""
|
|
564
564
|
try:
|
|
565
565
|
# Only add message if we have an image and provider supports it
|
|
566
|
-
if self.provider in [
|
|
566
|
+
if self.provider in [LLMProvider.OPENAI, LLMProvider.ANTHROPIC]:
|
|
567
567
|
image = parsed_screen.annotated_image_base64 or None
|
|
568
568
|
if image:
|
|
569
569
|
# Save screen info to current turn directory
|
|
@@ -577,7 +577,7 @@ class OmniLoop(BaseLoop):
|
|
|
577
577
|
logger.info(f"Saved elements to {elements_path}")
|
|
578
578
|
|
|
579
579
|
# Format the image content based on the provider
|
|
580
|
-
if self.provider ==
|
|
580
|
+
if self.provider == LLMProvider.ANTHROPIC:
|
|
581
581
|
# Compress the image before sending to Anthropic (5MB limit)
|
|
582
582
|
image_size = len(image)
|
|
583
583
|
logger.info(f"Image base64 is present, length: {image_size}")
|
|
@@ -731,7 +731,7 @@ class OmniLoop(BaseLoop):
|
|
|
731
731
|
action_type = f"hotkey_{content['Value'].replace('+', '_')}"
|
|
732
732
|
logger.info(f"Preparing hotkey with keys: {keys}")
|
|
733
733
|
# Get the method but call it with *args instead of **kwargs
|
|
734
|
-
method = getattr(self.computer, action)
|
|
734
|
+
method = getattr(self.computer.interface, action)
|
|
735
735
|
await method(*keys) # Unpack the keys list as positional arguments
|
|
736
736
|
logger.info(f"Tool execution completed successfully: {action}")
|
|
737
737
|
|
|
@@ -776,7 +776,7 @@ class OmniLoop(BaseLoop):
|
|
|
776
776
|
|
|
777
777
|
# Execute tool and handle result
|
|
778
778
|
try:
|
|
779
|
-
method = getattr(self.computer, action)
|
|
779
|
+
method = getattr(self.computer.interface, action)
|
|
780
780
|
logger.info(f"Found method for action '{action}': {method}")
|
|
781
781
|
await method(**kwargs)
|
|
782
782
|
logger.info(f"Tool execution completed successfully: {action}")
|
agent/providers/omni/parser.py
CHANGED
|
@@ -79,7 +79,7 @@ class OmniParser:
|
|
|
79
79
|
try:
|
|
80
80
|
# Get screenshot from computer
|
|
81
81
|
logger.info("Taking screenshot...")
|
|
82
|
-
screenshot = await computer.screenshot()
|
|
82
|
+
screenshot = await computer.interface.screenshot()
|
|
83
83
|
|
|
84
84
|
# Log screenshot info
|
|
85
85
|
logger.info(f"Screenshot type: {type(screenshot)}")
|
agent/providers/omni/prompts.py
CHANGED
|
@@ -62,17 +62,3 @@ IMPORTANT NOTES:
|
|
|
62
62
|
9. Reflect whether the element is clickable or not, for example reflect if it is an hyperlink or a button or a normal text.
|
|
63
63
|
10. If you are prompted with login information page or captcha page, or you think it need user's permission to do the next action, you should say "Action": "None" in the json field.
|
|
64
64
|
"""
|
|
65
|
-
|
|
66
|
-
# SYSTEM_PROMPT1 = """You are an AI assistant helping users interact with their computer.
|
|
67
|
-
# Analyze the screen information and respond with JSON containing:
|
|
68
|
-
# {
|
|
69
|
-
# "Box ID": "Numeric ID of the relevant UI element",
|
|
70
|
-
# "Action": "One of: left_click, right_click, double_click, move_cursor, drag_to, type_text, press_key, hotkey, scroll_down, scroll_up, wait",
|
|
71
|
-
# "Value": "Text to type, key to press",
|
|
72
|
-
# "Explanation": "Why this action was chosen"
|
|
73
|
-
# }
|
|
74
|
-
|
|
75
|
-
# Notes:
|
|
76
|
-
# - For starting applications, use the "hotkey" action with command+space for starting a Spotlight search.
|
|
77
|
-
# - Each UI element is highlighted with a colored bounding box, and its Box ID appears nearby in the same color for easy identification.
|
|
78
|
-
# """
|
agent/providers/omni/types.py
CHANGED
|
@@ -10,21 +10,18 @@ class LLMProvider(StrEnum):
|
|
|
10
10
|
|
|
11
11
|
ANTHROPIC = "anthropic"
|
|
12
12
|
OPENAI = "openai"
|
|
13
|
-
GROQ = "groq"
|
|
14
|
-
QWEN = "qwen"
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
APIProvider = LLMProvider
|
|
15
|
+
LLMProvider
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
@dataclass
|
|
22
19
|
class LLM:
|
|
23
20
|
"""Configuration for LLM model and provider."""
|
|
24
|
-
|
|
21
|
+
|
|
25
22
|
provider: LLMProvider
|
|
26
23
|
name: Optional[str] = None
|
|
27
|
-
|
|
24
|
+
|
|
28
25
|
def __post_init__(self):
|
|
29
26
|
"""Set default model name if not provided."""
|
|
30
27
|
if self.name is None:
|
|
@@ -40,14 +37,10 @@ Model = LLM
|
|
|
40
37
|
PROVIDER_TO_DEFAULT_MODEL: Dict[LLMProvider, str] = {
|
|
41
38
|
LLMProvider.ANTHROPIC: "claude-3-7-sonnet-20250219",
|
|
42
39
|
LLMProvider.OPENAI: "gpt-4o",
|
|
43
|
-
LLMProvider.GROQ: "deepseek-r1-distill-llama-70b",
|
|
44
|
-
LLMProvider.QWEN: "qwen2.5-vl-72b-instruct",
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
# Environment variable names for each provider
|
|
48
43
|
PROVIDER_TO_ENV_VAR: Dict[LLMProvider, str] = {
|
|
49
44
|
LLMProvider.ANTHROPIC: "ANTHROPIC_API_KEY",
|
|
50
45
|
LLMProvider.OPENAI: "OPENAI_API_KEY",
|
|
51
|
-
LLMProvider.GROQ: "GROQ_API_KEY",
|
|
52
|
-
LLMProvider.QWEN: "QWEN_API_KEY",
|
|
53
46
|
}
|
agent/telemetry.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Telemetry support for Agent class."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from core.telemetry import (
|
|
10
|
+
record_event,
|
|
11
|
+
is_telemetry_enabled,
|
|
12
|
+
flush,
|
|
13
|
+
get_telemetry_client,
|
|
14
|
+
increment,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# System information used for telemetry
|
|
18
|
+
SYSTEM_INFO = {
|
|
19
|
+
"os": sys.platform,
|
|
20
|
+
"python_version": platform.python_version(),
|
|
21
|
+
}
|
agent/types/base.py
CHANGED
|
@@ -44,9 +44,10 @@ class Annotation(BaseModel):
|
|
|
44
44
|
vm_url: str
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class
|
|
47
|
+
class AgentLoop(Enum):
|
|
48
48
|
"""Enumeration of available loop types."""
|
|
49
49
|
|
|
50
50
|
ANTHROPIC = auto() # Anthropic implementation
|
|
51
|
+
OPENAI = auto() # OpenAI implementation
|
|
51
52
|
OMNI = auto() # OmniLoop implementation
|
|
52
53
|
# Add more loop types as needed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cua-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CUA (Computer Use) Agent for AI-driven computer interaction
|
|
5
5
|
Author-Email: TryCua <gh@trycua.com>
|
|
6
6
|
Requires-Python: <3.13,>=3.10
|
|
@@ -13,6 +13,7 @@ Requires-Dist: pydantic<3.0.0,>=2.6.4
|
|
|
13
13
|
Requires-Dist: rich<14.0.0,>=13.7.1
|
|
14
14
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.1
|
|
15
15
|
Requires-Dist: cua-computer<0.2.0,>=0.1.0
|
|
16
|
+
Requires-Dist: cua-core<0.2.0,>=0.1.0
|
|
16
17
|
Requires-Dist: certifi>=2024.2.2
|
|
17
18
|
Provides-Extra: anthropic
|
|
18
19
|
Requires-Dist: anthropic>=0.49.0; extra == "anthropic"
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
agent/README.md,sha256=8EFnLrKejthEcL9bZflQSbvA-KwpiPanBz8TEEwRub8,2153
|
|
2
|
-
agent/__init__.py,sha256=
|
|
3
|
-
agent/core/README.md,sha256=
|
|
2
|
+
agent/__init__.py,sha256=iX9e3iSpdUtqZAK9smiqwo3OcipR2v5rlXQyDiXXVxQ,691
|
|
3
|
+
agent/core/README.md,sha256=VOXNVbR0ugxf9gCXYmZtUU2kngZhfi29haT_oSxK0Lk,3559
|
|
4
4
|
agent/core/__init__.py,sha256=0htZ-VfsH9ixHB8j_SXu_uv6r3XXsq5TrghFNd-yRNE,709
|
|
5
|
-
agent/core/agent.py,sha256=
|
|
6
|
-
agent/core/base_agent.py,sha256=
|
|
5
|
+
agent/core/agent.py,sha256=MN_A5gVS_E_1e9UFdRR8iKeR2xMfbbIL-9xK6w7GNwo,9703
|
|
6
|
+
agent/core/base_agent.py,sha256=te9rk2tJZpEhDUEB1xSaFqe1zeOjmzMdHF5LaUDP2K0,6276
|
|
7
7
|
agent/core/callbacks.py,sha256=VbGIf5QkHh3Q0KsLM6wv7hRdIA5WExTVYLm64bckyUA,4306
|
|
8
8
|
agent/core/computer_agent.py,sha256=JGLMl_PwImUttmQh2amdLlXHS9CUyZ9MW20J1Xid7dM,2417
|
|
9
|
-
agent/core/experiment.py,sha256=
|
|
9
|
+
agent/core/experiment.py,sha256=FKmSDyA2YFSrO3q-91ZT29Jm1lm24YCuK59wQ6z-6IM,7930
|
|
10
10
|
agent/core/factory.py,sha256=WraOEHWPXBSN4R3DO7M2ctyadodeA8tzHM3dUjdQ_3A,3441
|
|
11
|
-
agent/core/loop.py,sha256=
|
|
11
|
+
agent/core/loop.py,sha256=vhdlSy_hIY3-a92uTGdF3oYE5Qcq0U2hyTJNmXunnfc,9009
|
|
12
12
|
agent/core/messages.py,sha256=N8pV8Eh-AJpMuDPRI5OGWUIOU6DRr-pQjK9XU0go9Hk,7637
|
|
13
|
+
agent/core/telemetry.py,sha256=bOP3z74dpXwvn1bGCVxe67jwu1m-4nYmKlypAJovqCQ,4304
|
|
13
14
|
agent/core/tools/__init__.py,sha256=xZen-PqUp2dUaMEHJowXCQm33_5Sxhsx9PSoD0rq6tI,489
|
|
14
15
|
agent/core/tools/base.py,sha256=CdzRFNuOjNfzgyTUN4ZoCGkUDR5HI0ECQVpvrUdEij8,2295
|
|
15
16
|
agent/core/tools/bash.py,sha256=jnJKVlHn8np8e0gWd8EO0_qqjMkfQzutSugA_Iol4jE,1585
|
|
@@ -18,11 +19,11 @@ agent/core/tools/computer.py,sha256=lT_aW3huoYpcM8kffuokELupSz_WZG_qkaW1gITRC58,
|
|
|
18
19
|
agent/core/tools/edit.py,sha256=kv4jTKCM0VXrnoNErf7mT-xlr81-7T8v49_VA9y_L4Y,2005
|
|
19
20
|
agent/core/tools/manager.py,sha256=IRsCXjGc076nncQuyIjODoafnHTDhrf9sP5B4q5Pcdo,1742
|
|
20
21
|
agent/providers/__init__.py,sha256=b4tIBAaIB1V7p8V0BWipHVnMhfHH_OuVgP4OWGSHdD8,194
|
|
21
|
-
agent/providers/anthropic/__init__.py,sha256=
|
|
22
|
-
agent/providers/anthropic/api/client.py,sha256=
|
|
22
|
+
agent/providers/anthropic/__init__.py,sha256=Mj11IZnVshZ2iHkvg4Z5-jrQIaD1WvzDz2Zk_pMwqIA,149
|
|
23
|
+
agent/providers/anthropic/api/client.py,sha256=Y_g4Xg8Ko4tCqjipVm0GBMw-86vw0KQVXS5aWzJinzw,7038
|
|
23
24
|
agent/providers/anthropic/api/logging.py,sha256=vHpwkIyOZdkSTVIH4ycbBPd4a_rzhP7Osu1I-Ayouwc,5154
|
|
24
25
|
agent/providers/anthropic/callbacks/manager.py,sha256=dRKN7MuBze2dLal0iHDxCKYqMdh_KShSphuwn7zC-c4,1878
|
|
25
|
-
agent/providers/anthropic/loop.py,sha256
|
|
26
|
+
agent/providers/anthropic/loop.py,sha256=-g-OUpdVPSTO5kFJSZ5AmnjoWSEs2niHZFSR6B_KKvU,17904
|
|
26
27
|
agent/providers/anthropic/messages/manager.py,sha256=atD41v6bjC1STxRB-jLBty9wHlMwacH9cwsL4tBz3uo,4891
|
|
27
28
|
agent/providers/anthropic/prompts.py,sha256=nHFfgPrfvnWrEdVP7EUBGUHAI85D2X9HeZirk9EwncU,1941
|
|
28
29
|
agent/providers/anthropic/tools/__init__.py,sha256=JyZwuVtPUnZwRSZBSCdQv9yxbLCsygm3l8Ywjjt9qTQ,661
|
|
@@ -33,8 +34,8 @@ agent/providers/anthropic/tools/computer.py,sha256=WnQS2rIIDz1juwoQMun2ODJjOV134
|
|
|
33
34
|
agent/providers/anthropic/tools/edit.py,sha256=EGRP61MDA4Oue1D7Q-_vLpd6LdGbdBA1Z4HSZ66DbmI,13465
|
|
34
35
|
agent/providers/anthropic/tools/manager.py,sha256=zW-biqO_MV3fb1nDEOl3EmCXD1leoglFj6LDRSM3djs,1982
|
|
35
36
|
agent/providers/anthropic/tools/run.py,sha256=xhXdnBK1di9muaO44CEirL9hpGy3NmKbjfMpyeVmn8Y,1595
|
|
36
|
-
agent/providers/anthropic/types.py,sha256=
|
|
37
|
-
agent/providers/omni/__init__.py,sha256=
|
|
37
|
+
agent/providers/anthropic/types.py,sha256=SF00kOMC1ui8j9Ah56KaeiR2cL394qCHjFIsBpXxt5w,421
|
|
38
|
+
agent/providers/omni/__init__.py,sha256=eTUh4Pmh4zO-RLnP-wAFm8EkJBMImT-G2xnVIYWRti0,744
|
|
38
39
|
agent/providers/omni/callbacks.py,sha256=ZG9NCgsHWt6y5jKsfcGLaoLxTpmKnIhCArDdeP4q9sA,2369
|
|
39
40
|
agent/providers/omni/clients/anthropic.py,sha256=X_QRVxqwA_ExdUqgBEwo1aHOfZQxVIBDmDugNHF97OM,3554
|
|
40
41
|
agent/providers/omni/clients/base.py,sha256=zAAgPi0jl3SWPC730R9l79E8bfYPSo39UtCSE-mrK6I,1076
|
|
@@ -43,23 +44,24 @@ agent/providers/omni/clients/openai.py,sha256=E4TAXMUFoYTunJETCWCNx5XAc6xutiN4rB
|
|
|
43
44
|
agent/providers/omni/clients/utils.py,sha256=Ani9CVVBm_J2Dl51WG6p1GVuoI6cq8scISrG0pmQ37o,688
|
|
44
45
|
agent/providers/omni/experiment.py,sha256=JGAdHi7Nf73I48c9k3TY1Xpr_i6D2VG1wurOzw5cNGk,9888
|
|
45
46
|
agent/providers/omni/image_utils.py,sha256=qIFuNi5cIMVwrqYBXG1T6PxUlbxz7gIngFFP39bZIlU,2782
|
|
46
|
-
agent/providers/omni/loop.py,sha256=
|
|
47
|
+
agent/providers/omni/loop.py,sha256=72o7q92nO7i0EUrVhEPCEHprRKdBYsg5iLTLfLHXAsw,43847
|
|
47
48
|
agent/providers/omni/messages.py,sha256=zdjQCAMH-hOyrQQesHhTiIsQbw43KqVSmVIzS8JOIFA,6134
|
|
48
|
-
agent/providers/omni/parser.py,sha256=
|
|
49
|
-
agent/providers/omni/prompts.py,sha256=
|
|
49
|
+
agent/providers/omni/parser.py,sha256=lTAoSMSf2zpwqR_8W0SXG3cYIFeUiZa5vXdpjqZwEHY,9161
|
|
50
|
+
agent/providers/omni/prompts.py,sha256=Mupjy0bUwBjcAeLXpE1r1jisYPSlhwsp-IXJKEKrEtw,3779
|
|
50
51
|
agent/providers/omni/tool_manager.py,sha256=O6DxyEI-Vg6jt99phh011o4q4me_vNhH2YffIxkO4GM,2585
|
|
51
52
|
agent/providers/omni/tools/__init__.py,sha256=l636hx9Q5z9eaFdPanPwPENUE-w-Xm8kAZhPUq0ZQF4,309
|
|
52
53
|
agent/providers/omni/tools/bash.py,sha256=y_ibfP9iRcbiU_E0faAoa4DCP_BlkMlKOOURdBBIGZE,2030
|
|
53
54
|
agent/providers/omni/tools/computer.py,sha256=xkMmAR0e_kbf0Zs2mggCDyWrQOJZyXOKPFjkutaQb94,9108
|
|
54
55
|
agent/providers/omni/tools/manager.py,sha256=V_tav2yU92PyQnFlxNXG1wvNEaJoEYudtKx5sRjj06Q,2619
|
|
55
|
-
agent/providers/omni/types.py,sha256=
|
|
56
|
+
agent/providers/omni/types.py,sha256=rpr7-mH9VK1R-nJ6tVu1gKp427j-hw1DpHc197b44nU,1017
|
|
56
57
|
agent/providers/omni/utils.py,sha256=JqSye1bEp4wxhUgmaMyZi172fTlgXtygJ7XlnvKdUtE,6337
|
|
57
58
|
agent/providers/omni/visualization.py,sha256=N3qVQLxYmia3iSVC5oCt5YRlMPuVfylCOyB99R33u8U,3924
|
|
59
|
+
agent/telemetry.py,sha256=pVGxbj0ewnvq4EGj28CydN4a1iOfvZR_XKL3vIOqhOM,390
|
|
58
60
|
agent/types/__init__.py,sha256=61UFJT-w0CT4YRn0LiTx4A7fsMdVQjlXO9vnmbI1A7Y,604
|
|
59
|
-
agent/types/base.py,sha256=
|
|
61
|
+
agent/types/base.py,sha256=Iy_Q2DIBMLtwWdLyfvHw_6E2ltYu3bIv8GUNy3LYkGs,1133
|
|
60
62
|
agent/types/messages.py,sha256=4-hwtxeAhto90_EZpHFducddtsHUsHauvXzYrpKG4RE,953
|
|
61
63
|
agent/types/tools.py,sha256=Jes2CFCFqC727WWHbO-sG7V03rBHnQe5X7Oi9ZkuScI,877
|
|
62
|
-
cua_agent-0.1.
|
|
63
|
-
cua_agent-0.1.
|
|
64
|
-
cua_agent-0.1.
|
|
65
|
-
cua_agent-0.1.
|
|
64
|
+
cua_agent-0.1.3.dist-info/METADATA,sha256=jrj3DGxV6UHOSezXqLY2BhjmZxI1Eunv9aFXcoUQmgs,1928
|
|
65
|
+
cua_agent-0.1.3.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
|
66
|
+
cua_agent-0.1.3.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
67
|
+
cua_agent-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|