cua-computer 0.2.1__py3-none-any.whl → 0.2.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.
- computer/computer.py +96 -68
- computer/interface/base.py +5 -1
- computer/interface/factory.py +8 -4
- computer/interface/linux.py +31 -3
- computer/interface/macos.py +33 -5
- computer/providers/cloud/provider.py +38 -63
- computer/providers/factory.py +4 -6
- computer/providers/lumier/provider.py +7 -1
- computer/ui/gradio/app.py +43 -13
- {cua_computer-0.2.1.dist-info → cua_computer-0.2.3.dist-info}/METADATA +2 -2
- {cua_computer-0.2.1.dist-info → cua_computer-0.2.3.dist-info}/RECORD +13 -13
- {cua_computer-0.2.1.dist-info → cua_computer-0.2.3.dist-info}/WHEEL +0 -0
- {cua_computer-0.2.1.dist-info → cua_computer-0.2.3.dist-info}/entry_points.txt +0 -0
computer/computer.py
CHANGED
@@ -38,7 +38,8 @@ class Computer:
|
|
38
38
|
noVNC_port: Optional[int] = 8006,
|
39
39
|
host: str = os.environ.get("PYLUME_HOST", "localhost"),
|
40
40
|
storage: Optional[str] = None,
|
41
|
-
ephemeral: bool = False
|
41
|
+
ephemeral: bool = False,
|
42
|
+
api_key: Optional[str] = None
|
42
43
|
):
|
43
44
|
"""Initialize a new Computer instance.
|
44
45
|
|
@@ -77,12 +78,14 @@ class Computer:
|
|
77
78
|
self.os_type = os_type
|
78
79
|
self.provider_type = provider_type
|
79
80
|
self.ephemeral = ephemeral
|
81
|
+
|
82
|
+
self.api_key = api_key
|
80
83
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
# The default is currently to use non-ephemeral storage
|
85
|
+
if storage and ephemeral and storage != "ephemeral":
|
86
|
+
raise ValueError("Storage path and ephemeral flag cannot be used together")
|
87
|
+
self.storage = "ephemeral" if ephemeral else storage
|
88
|
+
|
86
89
|
# For Lumier provider, store the first shared directory path to use
|
87
90
|
# for VM file sharing
|
88
91
|
self.shared_path = None
|
@@ -256,9 +259,7 @@ class Computer:
|
|
256
259
|
elif self.provider_type == VMProviderType.CLOUD:
|
257
260
|
self.config.vm_provider = VMProviderFactory.create_provider(
|
258
261
|
self.provider_type,
|
259
|
-
|
260
|
-
host=host,
|
261
|
-
storage=storage,
|
262
|
+
api_key=self.api_key,
|
262
263
|
verbose=verbose,
|
263
264
|
)
|
264
265
|
else:
|
@@ -279,12 +280,14 @@ class Computer:
|
|
279
280
|
raise RuntimeError(f"Failed to initialize VM provider: {e}")
|
280
281
|
|
281
282
|
# Check if VM exists or create it
|
283
|
+
is_running = False
|
282
284
|
try:
|
283
285
|
if self.config.vm_provider is None:
|
284
286
|
raise RuntimeError(f"VM provider not initialized for {self.config.name}")
|
285
287
|
|
286
288
|
vm = await self.config.vm_provider.get_vm(self.config.name)
|
287
289
|
self.logger.verbose(f"Found existing VM: {self.config.name}")
|
290
|
+
is_running = vm.get("status") == "running"
|
288
291
|
except Exception as e:
|
289
292
|
self.logger.error(f"VM not found: {self.config.name}")
|
290
293
|
self.logger.error(f"Error: {e}")
|
@@ -292,63 +295,67 @@ class Computer:
|
|
292
295
|
f"VM {self.config.name} could not be found or created."
|
293
296
|
)
|
294
297
|
|
295
|
-
#
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
298
|
+
# Start the VM if it's not running
|
299
|
+
if not is_running:
|
300
|
+
self.logger.info(f"VM {self.config.name} is not running, starting it...")
|
301
|
+
|
302
|
+
# Convert paths to dictionary format for shared directories
|
303
|
+
shared_dirs = []
|
304
|
+
for path in self.shared_directories:
|
305
|
+
self.logger.verbose(f"Adding shared directory: {path}")
|
306
|
+
path = os.path.abspath(os.path.expanduser(path))
|
307
|
+
if os.path.exists(path):
|
308
|
+
# Add path in format expected by Lume API
|
309
|
+
shared_dirs.append({
|
310
|
+
"hostPath": path,
|
311
|
+
"readOnly": False
|
312
|
+
})
|
313
|
+
else:
|
314
|
+
self.logger.warning(f"Shared directory does not exist: {path}")
|
315
|
+
|
316
|
+
# Prepare run options to pass to the provider
|
317
|
+
run_opts = {}
|
318
|
+
|
319
|
+
# Add display information if available
|
320
|
+
if self.config.display is not None:
|
321
|
+
display_info = {
|
322
|
+
"width": self.config.display.width,
|
323
|
+
"height": self.config.display.height,
|
324
|
+
}
|
308
325
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
display_info = {
|
315
|
-
"width": self.config.display.width,
|
316
|
-
"height": self.config.display.height,
|
317
|
-
}
|
318
|
-
|
319
|
-
# Check if scale_factor exists before adding it
|
320
|
-
if hasattr(self.config.display, "scale_factor"):
|
321
|
-
display_info["scale_factor"] = self.config.display.scale_factor
|
322
|
-
|
323
|
-
run_opts["display"] = display_info
|
326
|
+
# Check if scale_factor exists before adding it
|
327
|
+
if hasattr(self.config.display, "scale_factor"):
|
328
|
+
display_info["scale_factor"] = self.config.display.scale_factor
|
329
|
+
|
330
|
+
run_opts["display"] = display_info
|
324
331
|
|
325
|
-
|
326
|
-
|
327
|
-
|
332
|
+
# Add shared directories if available
|
333
|
+
if self.shared_directories:
|
334
|
+
run_opts["shared_directories"] = shared_dirs.copy()
|
328
335
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
336
|
+
# Run the VM with the provider
|
337
|
+
try:
|
338
|
+
if self.config.vm_provider is None:
|
339
|
+
raise RuntimeError(f"VM provider not initialized for {self.config.name}")
|
340
|
+
|
341
|
+
# Use the complete run_opts we prepared earlier
|
342
|
+
# Handle ephemeral storage for run_vm method too
|
343
|
+
storage_param = "ephemeral" if self.ephemeral else self.storage
|
333
344
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
self.logger.info(f"VM run response: {response if response else 'None'}")
|
349
|
-
except Exception as run_error:
|
350
|
-
self.logger.error(f"Failed to run VM: {run_error}")
|
351
|
-
raise RuntimeError(f"Failed to start VM: {run_error}")
|
345
|
+
# Log the image being used
|
346
|
+
self.logger.info(f"Running VM using image: {self.image}")
|
347
|
+
|
348
|
+
# Call provider.run_vm with explicit image parameter
|
349
|
+
response = await self.config.vm_provider.run_vm(
|
350
|
+
image=self.image,
|
351
|
+
name=self.config.name,
|
352
|
+
run_opts=run_opts,
|
353
|
+
storage=storage_param
|
354
|
+
)
|
355
|
+
self.logger.info(f"VM run response: {response if response else 'None'}")
|
356
|
+
except Exception as run_error:
|
357
|
+
self.logger.error(f"Failed to run VM: {run_error}")
|
358
|
+
raise RuntimeError(f"Failed to start VM: {run_error}")
|
352
359
|
|
353
360
|
# Wait for VM to be ready with a valid IP address
|
354
361
|
self.logger.info("Waiting for VM to be ready with a valid IP address...")
|
@@ -386,12 +393,25 @@ class Computer:
|
|
386
393
|
self.logger.info(f"Initializing interface for {self.os_type} at {ip_address}")
|
387
394
|
from .interface.base import BaseComputerInterface
|
388
395
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
396
|
+
# Pass authentication credentials if using cloud provider
|
397
|
+
if self.provider_type == VMProviderType.CLOUD and self.api_key and self.config.name:
|
398
|
+
self._interface = cast(
|
399
|
+
BaseComputerInterface,
|
400
|
+
InterfaceFactory.create_interface_for_os(
|
401
|
+
os=self.os_type,
|
402
|
+
ip_address=ip_address,
|
403
|
+
api_key=self.api_key,
|
404
|
+
vm_name=self.config.name
|
405
|
+
),
|
406
|
+
)
|
407
|
+
else:
|
408
|
+
self._interface = cast(
|
409
|
+
BaseComputerInterface,
|
410
|
+
InterfaceFactory.create_interface_for_os(
|
411
|
+
os=self.os_type,
|
412
|
+
ip_address=ip_address
|
413
|
+
),
|
414
|
+
)
|
395
415
|
|
396
416
|
# Wait for the WebSocket interface to be ready
|
397
417
|
self.logger.info("Connecting to WebSocket interface...")
|
@@ -406,6 +426,9 @@ class Computer:
|
|
406
426
|
raise TimeoutError(
|
407
427
|
f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}"
|
408
428
|
)
|
429
|
+
# self.logger.warning(
|
430
|
+
# f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}, expect missing functionality"
|
431
|
+
# )
|
409
432
|
|
410
433
|
# Create an event to keep the VM running in background if needed
|
411
434
|
if not self.use_host_computer_server:
|
@@ -483,6 +506,11 @@ class Computer:
|
|
483
506
|
|
484
507
|
# Call the provider's get_ip method which will wait indefinitely
|
485
508
|
storage_param = "ephemeral" if self.ephemeral else self.storage
|
509
|
+
|
510
|
+
# Log the image being used
|
511
|
+
self.logger.info(f"Running VM using image: {self.image}")
|
512
|
+
|
513
|
+
# Call provider.get_ip with explicit image parameter
|
486
514
|
ip = await self.config.vm_provider.get_ip(
|
487
515
|
name=self.config.name,
|
488
516
|
storage=storage_param,
|
computer/interface/base.py
CHANGED
@@ -8,17 +8,21 @@ from ..logger import Logger, LogLevel
|
|
8
8
|
class BaseComputerInterface(ABC):
|
9
9
|
"""Base class for computer control interfaces."""
|
10
10
|
|
11
|
-
def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"):
|
11
|
+
def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
|
12
12
|
"""Initialize interface.
|
13
13
|
|
14
14
|
Args:
|
15
15
|
ip_address: IP address of the computer to control
|
16
16
|
username: Username for authentication
|
17
17
|
password: Password for authentication
|
18
|
+
api_key: Optional API key for cloud authentication
|
19
|
+
vm_name: Optional VM name for cloud authentication
|
18
20
|
"""
|
19
21
|
self.ip_address = ip_address
|
20
22
|
self.username = username
|
21
23
|
self.password = password
|
24
|
+
self.api_key = api_key
|
25
|
+
self.vm_name = vm_name
|
22
26
|
self.logger = Logger("cua.interface", LogLevel.NORMAL)
|
23
27
|
|
24
28
|
@abstractmethod
|
computer/interface/factory.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Factory for creating computer interfaces."""
|
2
2
|
|
3
|
-
from typing import Literal
|
3
|
+
from typing import Literal, Optional
|
4
4
|
from .base import BaseComputerInterface
|
5
5
|
|
6
6
|
class InterfaceFactory:
|
@@ -9,13 +9,17 @@ class InterfaceFactory:
|
|
9
9
|
@staticmethod
|
10
10
|
def create_interface_for_os(
|
11
11
|
os: Literal['macos', 'linux'],
|
12
|
-
ip_address: str
|
12
|
+
ip_address: str,
|
13
|
+
api_key: Optional[str] = None,
|
14
|
+
vm_name: Optional[str] = None
|
13
15
|
) -> BaseComputerInterface:
|
14
16
|
"""Create an interface for the specified OS.
|
15
17
|
|
16
18
|
Args:
|
17
19
|
os: Operating system type ('macos' or 'linux')
|
18
20
|
ip_address: IP address of the computer to control
|
21
|
+
api_key: Optional API key for cloud authentication
|
22
|
+
vm_name: Optional VM name for cloud authentication
|
19
23
|
|
20
24
|
Returns:
|
21
25
|
BaseComputerInterface: The appropriate interface for the OS
|
@@ -28,8 +32,8 @@ class InterfaceFactory:
|
|
28
32
|
from .linux import LinuxComputerInterface
|
29
33
|
|
30
34
|
if os == 'macos':
|
31
|
-
return MacOSComputerInterface(ip_address)
|
35
|
+
return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
|
32
36
|
elif os == 'linux':
|
33
|
-
return LinuxComputerInterface(ip_address)
|
37
|
+
return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
|
34
38
|
else:
|
35
39
|
raise ValueError(f"Unsupported OS type: {os}")
|
computer/interface/linux.py
CHANGED
@@ -15,8 +15,8 @@ from .models import Key, KeyType
|
|
15
15
|
class LinuxComputerInterface(BaseComputerInterface):
|
16
16
|
"""Interface for Linux."""
|
17
17
|
|
18
|
-
def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"):
|
19
|
-
super().__init__(ip_address, username, password)
|
18
|
+
def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
|
19
|
+
super().__init__(ip_address, username, password, api_key, vm_name)
|
20
20
|
self._ws = None
|
21
21
|
self._reconnect_task = None
|
22
22
|
self._closed = False
|
@@ -37,7 +37,9 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
37
37
|
Returns:
|
38
38
|
WebSocket URI for the Computer API Server
|
39
39
|
"""
|
40
|
-
|
40
|
+
protocol = "wss" if self.api_key else "ws"
|
41
|
+
port = "8443" if self.api_key else "8000"
|
42
|
+
return f"{protocol}://{self.ip_address}:{port}/ws"
|
41
43
|
|
42
44
|
async def _keep_alive(self):
|
43
45
|
"""Keep the WebSocket connection alive with automatic reconnection."""
|
@@ -86,6 +88,32 @@ class LinuxComputerInterface(BaseComputerInterface):
|
|
86
88
|
timeout=30,
|
87
89
|
)
|
88
90
|
self.logger.info("WebSocket connection established")
|
91
|
+
|
92
|
+
# If api_key and vm_name are provided, perform authentication handshake
|
93
|
+
if self.api_key and self.vm_name:
|
94
|
+
self.logger.info("Performing authentication handshake...")
|
95
|
+
auth_message = {
|
96
|
+
"command": "authenticate",
|
97
|
+
"params": {
|
98
|
+
"api_key": self.api_key,
|
99
|
+
"container_name": self.vm_name
|
100
|
+
}
|
101
|
+
}
|
102
|
+
await self._ws.send(json.dumps(auth_message))
|
103
|
+
|
104
|
+
# Wait for authentication response
|
105
|
+
auth_response = await asyncio.wait_for(self._ws.recv(), timeout=10)
|
106
|
+
auth_result = json.loads(auth_response)
|
107
|
+
|
108
|
+
if not auth_result.get("success"):
|
109
|
+
error_msg = auth_result.get("error", "Authentication failed")
|
110
|
+
self.logger.error(f"Authentication failed: {error_msg}")
|
111
|
+
await self._ws.close()
|
112
|
+
self._ws = None
|
113
|
+
raise ConnectionError(f"Authentication failed: {error_msg}")
|
114
|
+
|
115
|
+
self.logger.info("Authentication successful")
|
116
|
+
|
89
117
|
self._reconnect_delay = 1 # Reset reconnect delay on successful connection
|
90
118
|
self._last_ping = time.time()
|
91
119
|
retry_count = 0 # Reset retry count on successful connection
|
computer/interface/macos.py
CHANGED
@@ -13,10 +13,10 @@ from .models import Key, KeyType
|
|
13
13
|
|
14
14
|
|
15
15
|
class MacOSComputerInterface(BaseComputerInterface):
|
16
|
-
"""Interface for
|
16
|
+
"""Interface for macOS."""
|
17
17
|
|
18
|
-
def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"):
|
19
|
-
super().__init__(ip_address, username, password)
|
18
|
+
def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
|
19
|
+
super().__init__(ip_address, username, password, api_key, vm_name)
|
20
20
|
self._ws = None
|
21
21
|
self._reconnect_task = None
|
22
22
|
self._closed = False
|
@@ -27,7 +27,7 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
27
27
|
self._max_reconnect_delay = 30 # Maximum delay between reconnection attempts
|
28
28
|
self._log_connection_attempts = True # Flag to control connection attempt logging
|
29
29
|
|
30
|
-
# Set logger name for
|
30
|
+
# Set logger name for macOS interface
|
31
31
|
self.logger = Logger("cua.interface.macos", LogLevel.NORMAL)
|
32
32
|
|
33
33
|
@property
|
@@ -37,7 +37,9 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
37
37
|
Returns:
|
38
38
|
WebSocket URI for the Computer API Server
|
39
39
|
"""
|
40
|
-
|
40
|
+
protocol = "wss" if self.api_key else "ws"
|
41
|
+
port = "8443" if self.api_key else "8000"
|
42
|
+
return f"{protocol}://{self.ip_address}:{port}/ws"
|
41
43
|
|
42
44
|
async def _keep_alive(self):
|
43
45
|
"""Keep the WebSocket connection alive with automatic reconnection."""
|
@@ -86,6 +88,32 @@ class MacOSComputerInterface(BaseComputerInterface):
|
|
86
88
|
timeout=30,
|
87
89
|
)
|
88
90
|
self.logger.info("WebSocket connection established")
|
91
|
+
|
92
|
+
# If api_key and vm_name are provided, perform authentication handshake
|
93
|
+
if self.api_key and self.vm_name:
|
94
|
+
self.logger.info("Performing authentication handshake...")
|
95
|
+
auth_message = {
|
96
|
+
"command": "authenticate",
|
97
|
+
"params": {
|
98
|
+
"api_key": self.api_key,
|
99
|
+
"container_name": self.vm_name
|
100
|
+
}
|
101
|
+
}
|
102
|
+
await self._ws.send(json.dumps(auth_message))
|
103
|
+
|
104
|
+
# Wait for authentication response
|
105
|
+
auth_response = await asyncio.wait_for(self._ws.recv(), timeout=10)
|
106
|
+
auth_result = json.loads(auth_response)
|
107
|
+
|
108
|
+
if not auth_result.get("success"):
|
109
|
+
error_msg = auth_result.get("error", "Authentication failed")
|
110
|
+
self.logger.error(f"Authentication failed: {error_msg}")
|
111
|
+
await self._ws.close()
|
112
|
+
self._ws = None
|
113
|
+
raise ConnectionError(f"Authentication failed: {error_msg}")
|
114
|
+
|
115
|
+
self.logger.info("Authentication successful")
|
116
|
+
|
89
117
|
self._reconnect_delay = 1 # Reset reconnect delay on successful connection
|
90
118
|
self._last_ping = time.time()
|
91
119
|
retry_count = 0 # Reset retry count on successful connection
|
@@ -11,90 +11,65 @@ from ..base import BaseVMProvider, VMProviderType
|
|
11
11
|
# Setup logging
|
12
12
|
logger = logging.getLogger(__name__)
|
13
13
|
|
14
|
+
import asyncio
|
15
|
+
import aiohttp
|
16
|
+
from urllib.parse import urlparse
|
17
|
+
|
14
18
|
class CloudProvider(BaseVMProvider):
|
15
|
-
"""Cloud VM Provider
|
16
|
-
|
17
|
-
This is a placeholder for a future cloud VM provider implementation.
|
18
|
-
"""
|
19
|
-
|
19
|
+
"""Cloud VM Provider implementation."""
|
20
20
|
def __init__(
|
21
|
-
self,
|
22
|
-
|
23
|
-
port: int = 7777,
|
24
|
-
storage: Optional[str] = None,
|
21
|
+
self,
|
22
|
+
api_key: str,
|
25
23
|
verbose: bool = False,
|
24
|
+
**kwargs,
|
26
25
|
):
|
27
|
-
"""
|
28
|
-
|
26
|
+
"""
|
29
27
|
Args:
|
30
|
-
|
31
|
-
|
32
|
-
storage: Path to store VM data
|
28
|
+
api_key: API key for authentication
|
29
|
+
name: Name of the VM
|
33
30
|
verbose: Enable verbose logging
|
34
31
|
"""
|
35
|
-
|
36
|
-
self.
|
37
|
-
self.storage = storage
|
32
|
+
assert api_key, "api_key required for CloudProvider"
|
33
|
+
self.api_key = api_key
|
38
34
|
self.verbose = verbose
|
39
|
-
|
40
|
-
logger.warning("CloudProvider is not yet implemented")
|
41
|
-
|
35
|
+
|
42
36
|
@property
|
43
37
|
def provider_type(self) -> VMProviderType:
|
44
|
-
"""Get the provider type."""
|
45
38
|
return VMProviderType.CLOUD
|
46
|
-
|
39
|
+
|
47
40
|
async def __aenter__(self):
|
48
|
-
"""Enter async context manager."""
|
49
|
-
logger.debug("Entering CloudProvider context")
|
50
41
|
return self
|
51
|
-
|
42
|
+
|
52
43
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
53
|
-
|
54
|
-
|
55
|
-
|
44
|
+
pass
|
45
|
+
|
56
46
|
async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
57
|
-
"""Get VM
|
58
|
-
|
59
|
-
|
60
|
-
"name": name,
|
61
|
-
"status": "unavailable",
|
62
|
-
"message": "CloudProvider is not implemented"
|
63
|
-
}
|
64
|
-
|
47
|
+
"""Get VM VNC URL by name using the cloud API."""
|
48
|
+
return {"name": name, "hostname": f"{name}.containers.cloud.trycua.com"}
|
49
|
+
|
65
50
|
async def list_vms(self) -> List[Dict[str, Any]]:
|
66
|
-
"""List all available VMs."""
|
67
51
|
logger.warning("CloudProvider.list_vms is not implemented")
|
68
52
|
return []
|
69
|
-
|
53
|
+
|
70
54
|
async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
71
|
-
"""Run a VM with the given options."""
|
72
55
|
logger.warning("CloudProvider.run_vm is not implemented")
|
73
|
-
return {
|
74
|
-
|
75
|
-
"status": "unavailable",
|
76
|
-
"message": "CloudProvider is not implemented"
|
77
|
-
}
|
78
|
-
|
56
|
+
return {"name": name, "status": "unavailable", "message": "CloudProvider is not implemented"}
|
57
|
+
|
79
58
|
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
80
|
-
"""Stop a running VM."""
|
81
59
|
logger.warning("CloudProvider.stop_vm is not implemented")
|
82
|
-
return {
|
83
|
-
|
84
|
-
"status": "stopped",
|
85
|
-
"message": "CloudProvider is not implemented"
|
86
|
-
}
|
87
|
-
|
60
|
+
return {"name": name, "status": "stopped", "message": "CloudProvider is not implemented"}
|
61
|
+
|
88
62
|
async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
89
|
-
"""Update VM configuration."""
|
90
63
|
logger.warning("CloudProvider.update_vm is not implemented")
|
91
|
-
return {
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
}
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
64
|
+
return {"name": name, "status": "unchanged", "message": "CloudProvider is not implemented"}
|
65
|
+
|
66
|
+
async def get_ip(self, name: Optional[str] = None, storage: Optional[str] = None, retry_delay: int = 2) -> str:
|
67
|
+
"""
|
68
|
+
Return the VM's IP address as '{container_name}.containers.cloud.trycua.com'.
|
69
|
+
Uses the provided 'name' argument (the VM name requested by the caller),
|
70
|
+
falling back to self.name only if 'name' is None.
|
71
|
+
Retries up to 3 times with retry_delay seconds if hostname is not available.
|
72
|
+
"""
|
73
|
+
if name is None:
|
74
|
+
raise ValueError("VM name is required for CloudProvider.get_ip")
|
75
|
+
return f"{name}.containers.cloud.trycua.com"
|
computer/providers/factory.py
CHANGED
@@ -22,7 +22,8 @@ class VMProviderFactory:
|
|
22
22
|
image: Optional[str] = None,
|
23
23
|
verbose: bool = False,
|
24
24
|
ephemeral: bool = False,
|
25
|
-
noVNC_port: Optional[int] = None
|
25
|
+
noVNC_port: Optional[int] = None,
|
26
|
+
**kwargs,
|
26
27
|
) -> BaseVMProvider:
|
27
28
|
"""Create a VM provider of the specified type.
|
28
29
|
|
@@ -101,12 +102,9 @@ class VMProviderFactory:
|
|
101
102
|
elif provider_type == VMProviderType.CLOUD:
|
102
103
|
try:
|
103
104
|
from .cloud import CloudProvider
|
104
|
-
# Return the stub implementation of CloudProvider
|
105
105
|
return CloudProvider(
|
106
|
-
|
107
|
-
|
108
|
-
storage=storage,
|
109
|
-
verbose=verbose
|
106
|
+
verbose=verbose,
|
107
|
+
**kwargs,
|
110
108
|
)
|
111
109
|
except ImportError as e:
|
112
110
|
logger.error(f"Failed to import CloudProvider: {e}")
|
@@ -344,9 +344,15 @@ class LumierProvider(BaseVMProvider):
|
|
344
344
|
# Use the VM image passed from the Computer class
|
345
345
|
print(f"Using VM image: {self.image}")
|
346
346
|
|
347
|
+
# If ghcr.io is in the image, use the full image name
|
348
|
+
if "ghcr.io" in self.image:
|
349
|
+
vm_image = self.image
|
350
|
+
else:
|
351
|
+
vm_image = f"ghcr.io/trycua/{self.image}"
|
352
|
+
|
347
353
|
cmd.extend([
|
348
354
|
"-e", f"VM_NAME={self.container_name}",
|
349
|
-
"-e", f"VERSION=
|
355
|
+
"-e", f"VERSION={vm_image}",
|
350
356
|
"-e", f"CPU_CORES={run_opts.get('cpu', '4')}",
|
351
357
|
"-e", f"RAM_SIZE={memory_mb}",
|
352
358
|
])
|
computer/ui/gradio/app.py
CHANGED
@@ -17,7 +17,7 @@ import base64
|
|
17
17
|
from datetime import datetime
|
18
18
|
from PIL import Image
|
19
19
|
from huggingface_hub import DatasetCard, DatasetCardData
|
20
|
-
from computer import Computer
|
20
|
+
from computer import Computer, VMProviderType
|
21
21
|
from gradio.components import ChatMessage
|
22
22
|
import pandas as pd
|
23
23
|
from datasets import Dataset, Features, Sequence, concatenate_datasets
|
@@ -528,21 +528,44 @@ async def execute(name, action, arguments):
|
|
528
528
|
|
529
529
|
return results
|
530
530
|
|
531
|
-
async def handle_init_computer():
|
532
|
-
"""Initialize the computer instance and tools"""
|
531
|
+
async def handle_init_computer(os_choice: str):
|
532
|
+
"""Initialize the computer instance and tools for macOS or Ubuntu"""
|
533
533
|
global computer, tool_call_logs, tools
|
534
|
-
|
535
|
-
|
534
|
+
|
535
|
+
if os_choice == "Ubuntu":
|
536
|
+
computer = Computer(
|
537
|
+
image="ubuntu-noble-vanilla:latest",
|
538
|
+
os_type="linux",
|
539
|
+
provider_type=VMProviderType.LUME,
|
540
|
+
display="1024x768",
|
541
|
+
memory="8GB",
|
542
|
+
cpu="4"
|
543
|
+
)
|
544
|
+
os_type_str = "linux"
|
545
|
+
image_str = "ubuntu-noble-vanilla:latest"
|
546
|
+
else:
|
547
|
+
computer = Computer(
|
548
|
+
image="macos-sequoia-cua:latest",
|
549
|
+
os_type="macos",
|
550
|
+
provider_type=VMProviderType.LUME,
|
551
|
+
display="1024x768",
|
552
|
+
memory="8GB",
|
553
|
+
cpu="4"
|
554
|
+
)
|
555
|
+
os_type_str = "macos"
|
556
|
+
image_str = "macos-sequoia-cua:latest"
|
557
|
+
|
536
558
|
await computer.run()
|
537
|
-
|
559
|
+
|
538
560
|
# Log computer initialization as a tool call
|
539
561
|
result = await execute("computer", "initialize", {
|
540
|
-
"os":
|
541
|
-
"
|
542
|
-
"
|
562
|
+
"os": os_type_str,
|
563
|
+
"image": image_str,
|
564
|
+
"display": "1024x768",
|
565
|
+
"memory": "8GB",
|
543
566
|
"cpu": "4"
|
544
567
|
})
|
545
|
-
|
568
|
+
|
546
569
|
return result["screenshot"], json.dumps(tool_call_logs, indent=2)
|
547
570
|
|
548
571
|
async def handle_screenshot():
|
@@ -1004,8 +1027,15 @@ def create_gradio_ui():
|
|
1004
1027
|
run_setup_btn = gr.Button("⚙️ Run Task Setup")
|
1005
1028
|
# Setup status textbox
|
1006
1029
|
setup_status = gr.Textbox(label="Setup Status", value="")
|
1007
|
-
|
1008
|
-
|
1030
|
+
|
1031
|
+
with gr.Group():
|
1032
|
+
os_choice = gr.Radio(
|
1033
|
+
label="OS",
|
1034
|
+
choices=["macOS", "Ubuntu"],
|
1035
|
+
value="macOS",
|
1036
|
+
interactive=False # disable until the ubuntu image is ready
|
1037
|
+
)
|
1038
|
+
start_btn = gr.Button("Initialize Computer")
|
1009
1039
|
|
1010
1040
|
with gr.Group():
|
1011
1041
|
input_text = gr.Textbox(label="Type Text")
|
@@ -1169,7 +1199,7 @@ def create_gradio_ui():
|
|
1169
1199
|
)
|
1170
1200
|
|
1171
1201
|
img.select(handle_click, inputs=[img, click_type], outputs=[img, action_log])
|
1172
|
-
start_btn.click(handle_init_computer, outputs=[img, action_log])
|
1202
|
+
start_btn.click(handle_init_computer, inputs=[os_choice], outputs=[img, action_log])
|
1173
1203
|
wait_btn.click(handle_wait, outputs=[img, action_log])
|
1174
1204
|
|
1175
1205
|
# DONE and FAIL buttons just do a placeholder action
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: cua-computer
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: Computer-Use Interface (CUI) framework powering Cua
|
5
5
|
Author-Email: TryCua <gh@trycua.com>
|
6
6
|
Requires-Python: >=3.10
|
@@ -78,7 +78,7 @@ finally:
|
|
78
78
|
To install the Computer-Use Interface (CUI):
|
79
79
|
|
80
80
|
```bash
|
81
|
-
pip install cua-computer
|
81
|
+
pip install "cua-computer[all]"
|
82
82
|
```
|
83
83
|
|
84
84
|
The `cua-computer` PyPi package pulls automatically the latest executable version of Lume through [pylume](https://github.com/trycua/pylume).
|
@@ -1,29 +1,29 @@
|
|
1
1
|
computer/__init__.py,sha256=QOxNrrJAuLRnsUC2zIFgRfzVSuDSXiYHlEF-9vkhV0o,1241
|
2
|
-
computer/computer.py,sha256=
|
2
|
+
computer/computer.py,sha256=Rc32XFZdKr7XZKO0zhbEom-REvYYPPlvmvjDbw5gP9k,32218
|
3
3
|
computer/interface/__init__.py,sha256=xQvYjq5PMn9ZJOmRR5mWtONTl_0HVd8ACvW6AQnzDdw,262
|
4
|
-
computer/interface/base.py,sha256=
|
5
|
-
computer/interface/factory.py,sha256=
|
6
|
-
computer/interface/linux.py,sha256=
|
7
|
-
computer/interface/macos.py,sha256=
|
4
|
+
computer/interface/base.py,sha256=CD9WpDp-6qP-ID5MjhXA8qpYs0XhJ4TPkR917l2FFSo,6021
|
5
|
+
computer/interface/factory.py,sha256=RjAZAB_jFuS8JierYjLbapRX6RqFE0qE3BiIyP5UDOE,1441
|
6
|
+
computer/interface/linux.py,sha256=EIVxwD_q0OmZoaW0Tv8FvKTpja9kInIsKWI47gpn2Po,27077
|
7
|
+
computer/interface/macos.py,sha256=_8R_IroxbcVmh1WagrjDQOitaT6tVkCHVzGgA_lwTrM,27077
|
8
8
|
computer/interface/models.py,sha256=RZKVUdwKrKUoFqwlx2Dk8Egkmq_AInlIu_d0xg7SZzw,3238
|
9
9
|
computer/logger.py,sha256=UVvnmZGOWVF9TCsixEbeQnDZ3wBPAJ2anW3Zp-MoJ8Y,2896
|
10
10
|
computer/models.py,sha256=iFNM1QfZArD8uf66XJXb2EDIREsfrxqqA5_liLBMfrE,1188
|
11
11
|
computer/providers/__init__.py,sha256=hS9lLxmmHa1u82XJJ_xuqSKipClsYUEPx-8OK9ogtVg,194
|
12
12
|
computer/providers/base.py,sha256=pUrM5aVBSUQS2TbfoiyMkNz20dWN7-F_aEjSf8EiJRc,3623
|
13
13
|
computer/providers/cloud/__init__.py,sha256=SDAcfhI2BlmVBrBZOHxQd3i1bJZjMIfl7QgmqjXa4z8,144
|
14
|
-
computer/providers/cloud/provider.py,sha256=
|
15
|
-
computer/providers/factory.py,sha256=
|
14
|
+
computer/providers/cloud/provider.py,sha256=gpBl_ZVbwk-0FhYycne-69KslnrAoDSZcyzetpLfiKE,2864
|
15
|
+
computer/providers/factory.py,sha256=9qVdt-fIovSNOokGMZ_2B1VPCLSZeDky4edcXyelZy4,4616
|
16
16
|
computer/providers/lume/__init__.py,sha256=E6hTbVQF5lLZD8JyG4rTwUnCBO4q9K8UkYNQ31R0h7c,193
|
17
17
|
computer/providers/lume/provider.py,sha256=grLZeXd4Y8iYsNq2gfNGcQq1bnTcNYNepEv-mxmROG4,20562
|
18
18
|
computer/providers/lume_api.py,sha256=qLYFYdWtWVxWjMq8baiqlIW2EUaen4Gl2Tc1Qr_QEig,20196
|
19
19
|
computer/providers/lumier/__init__.py,sha256=qz8coMA2K5MVoqNC12SDXJe6lI7z2pn6RHssUOMY5Ug,212
|
20
|
-
computer/providers/lumier/provider.py,sha256=
|
20
|
+
computer/providers/lumier/provider.py,sha256=CXwAKwJfR9ALFGM5u7UIZ-YrFwPvew_01wTe7dVmVbQ,46751
|
21
21
|
computer/telemetry.py,sha256=FvNFpxgeRuCMdNpREuSL7bOMZy9gSzY4J0rLeNDw0CU,3746
|
22
22
|
computer/ui/__init__.py,sha256=pmo05ek9qiB_x7DPeE6Vf_8RsIOqTD0w1dBLMHfoOnY,45
|
23
23
|
computer/ui/gradio/__init__.py,sha256=5_KimixM48-X74FCsLw7LbSt39MQfUMEL8-M9amK3Cw,117
|
24
|
-
computer/ui/gradio/app.py,sha256=
|
24
|
+
computer/ui/gradio/app.py,sha256=o31nphBcb6zM5OKPuODTjuOzSJ3lt61kQHpUeMBBs70,65077
|
25
25
|
computer/utils.py,sha256=zY50NXB7r51GNLQ6l7lhG_qv0_ufpQ8n0-SDhCei8m4,2838
|
26
|
-
cua_computer-0.2.
|
27
|
-
cua_computer-0.2.
|
28
|
-
cua_computer-0.2.
|
29
|
-
cua_computer-0.2.
|
26
|
+
cua_computer-0.2.3.dist-info/METADATA,sha256=zStwBXVos0iY6yfYVYeKg9ogTELmf3BW8_i_a1EYWg0,5844
|
27
|
+
cua_computer-0.2.3.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
28
|
+
cua_computer-0.2.3.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
29
|
+
cua_computer-0.2.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|