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 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
- if ephemeral:
82
- self.storage = "ephemeral"
83
- else:
84
- self.storage = storage
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
- port=port,
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
- # Convert paths to dictionary format for shared directories
296
- shared_dirs = []
297
- for path in self.shared_directories:
298
- self.logger.verbose(f"Adding shared directory: {path}")
299
- path = os.path.abspath(os.path.expanduser(path))
300
- if os.path.exists(path):
301
- # Add path in format expected by Lume API
302
- shared_dirs.append({
303
- "hostPath": path,
304
- "readOnly": False
305
- })
306
- else:
307
- self.logger.warning(f"Shared directory does not exist: {path}")
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
- # Prepare run options to pass to the provider
310
- run_opts = {}
311
-
312
- # Add display information if available
313
- if self.config.display is not None:
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
- # Add shared directories if available
326
- if self.shared_directories:
327
- run_opts["shared_directories"] = shared_dirs.copy()
332
+ # Add shared directories if available
333
+ if self.shared_directories:
334
+ run_opts["shared_directories"] = shared_dirs.copy()
328
335
 
329
- # Run the VM with the provider
330
- try:
331
- if self.config.vm_provider is None:
332
- raise RuntimeError(f"VM provider not initialized for {self.config.name}")
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
- # Use the complete run_opts we prepared earlier
335
- # Handle ephemeral storage for run_vm method too
336
- storage_param = "ephemeral" if self.ephemeral else self.storage
337
-
338
- # Log the image being used
339
- self.logger.info(f"Running VM using image: {self.image}")
340
-
341
- # Call provider.run_vm with explicit image parameter
342
- response = await self.config.vm_provider.run_vm(
343
- image=self.image,
344
- name=self.config.name,
345
- run_opts=run_opts,
346
- storage=storage_param
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
- self._interface = cast(
390
- BaseComputerInterface,
391
- InterfaceFactory.create_interface_for_os(
392
- os=self.os_type, ip_address=ip_address # type: ignore[arg-type]
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,
@@ -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
@@ -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}")
@@ -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
- return f"ws://{self.ip_address}:8000/ws"
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
@@ -13,10 +13,10 @@ from .models import Key, KeyType
13
13
 
14
14
 
15
15
  class MacOSComputerInterface(BaseComputerInterface):
16
- """Interface for MacOS."""
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 MacOS interface
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
- return f"ws://{self.ip_address}:8000/ws"
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 stub implementation.
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
- host: str = "localhost",
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
- """Initialize the Cloud provider.
28
-
26
+ """
29
27
  Args:
30
- host: Host to use for API connections (default: localhost)
31
- port: Port for the API server (default: 7777)
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
- self.host = host
36
- self.port = port
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
- """Exit async context manager."""
54
- logger.debug("Exiting CloudProvider context")
55
-
44
+ pass
45
+
56
46
  async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
57
- """Get VM information by name."""
58
- logger.warning("CloudProvider.get_vm is not implemented")
59
- return {
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
- "name": name,
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
- "name": name,
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
- "name": name,
93
- "status": "unchanged",
94
- "message": "CloudProvider is not implemented"
95
- }
96
-
97
- async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
98
- """Get the IP address of a VM."""
99
- logger.warning("CloudProvider.get_ip is not implemented")
100
- raise NotImplementedError("CloudProvider.get_ip is not implemented")
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"
@@ -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
- host=host,
107
- port=port,
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=ghcr.io/trycua/{self.image}",
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
- computer = Computer(os_type="macos", display="1024x768", memory="8GB", cpu="4")
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": "macos",
541
- "display": "1024x768",
542
- "memory": "8GB",
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
- start_btn = gr.Button("Initialize Computer")
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.1
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=zOc2cqZeDLKygNFuNCLn7FPBm5_dYHJqYBChV0TscK0,30626
2
+ computer/computer.py,sha256=Rc32XFZdKr7XZKO0zhbEom-REvYYPPlvmvjDbw5gP9k,32218
3
3
  computer/interface/__init__.py,sha256=xQvYjq5PMn9ZJOmRR5mWtONTl_0HVd8ACvW6AQnzDdw,262
4
- computer/interface/base.py,sha256=uRF2AfF3dbpOmvzZ55JgODpNC6LiudfhK8KCf5v7uUw,5771
5
- computer/interface/factory.py,sha256=2zUgFTKbhGqDbFKAcpOu47sDYmi4qidHegSQbfEAg1A,1159
6
- computer/interface/linux.py,sha256=5CyC0o-XR-TcelB5MPdzL20qSrT0C8kFYJxDupY3MWI,25350
7
- computer/interface/macos.py,sha256=WiK8hkJJwb6E6hFOMO30HVs6S2qSsPMM6f47cFj19OQ,25350
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=Cgjb7vFMk4OxG8TKOMYWO8v3v1s7WtWXppNwQRm97Cw,3523
15
- computer/providers/factory.py,sha256=FskJGkdhl_arlGr2wd_e4Adi_L14VTkvPTK2gTuUhT0,4731
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=qBhaI_AFeS2ksLp8gHpHyts_HHSG8TduT6dPfBQ29hA,46547
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=IU4gmvXvUxJxt1MSAMT6bfNC2YjT_JQbQ0M60bLygMs,64086
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.1.dist-info/METADATA,sha256=GWVENklYmRbA3s69dj34Jxh-TJtobQhpt4vPwyxmpNY,5837
27
- cua_computer-0.2.1.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
28
- cua_computer-0.2.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
29
- cua_computer-0.2.1.dist-info/RECORD,,
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,,