cua-computer 0.3.4__py3-none-any.whl → 0.3.5__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
@@ -753,7 +753,7 @@ class Computer:
753
753
 
754
754
 
755
755
  # Add virtual environment management functions to computer interface
756
- async def venv_install(self, venv_name: str, requirements: list[str]) -> tuple[str, str]:
756
+ async def venv_install(self, venv_name: str, requirements: list[str]):
757
757
  """Install packages in a virtual environment.
758
758
 
759
759
  Args:
@@ -771,14 +771,14 @@ class Computer:
771
771
 
772
772
  # Check if venv exists, if not create it
773
773
  check_cmd = f"test -d {venv_path} || ({create_cmd})"
774
- _, _ = await self.interface.run_command(check_cmd)
774
+ _ = await self.interface.run_command(check_cmd)
775
775
 
776
776
  # Install packages
777
777
  requirements_str = " ".join(requirements)
778
778
  install_cmd = f". {venv_path}/bin/activate && pip install {requirements_str}"
779
779
  return await self.interface.run_command(install_cmd)
780
780
 
781
- async def venv_cmd(self, venv_name: str, command: str) -> tuple[str, str]:
781
+ async def venv_cmd(self, venv_name: str, command: str):
782
782
  """Execute a shell command in a virtual environment.
783
783
 
784
784
  Args:
@@ -792,9 +792,9 @@ class Computer:
792
792
 
793
793
  # Check if virtual environment exists
794
794
  check_cmd = f"test -d {venv_path}"
795
- stdout, stderr = await self.interface.run_command(check_cmd)
795
+ result = await self.interface.run_command(check_cmd)
796
796
 
797
- if stderr or "test:" in stdout: # venv doesn't exist
797
+ if result.stderr or "test:" in result.stdout: # venv doesn't exist
798
798
  return "", f"Virtual environment '{venv_name}' does not exist. Create it first using venv_install."
799
799
 
800
800
  # Activate virtual environment and run command
@@ -890,21 +890,21 @@ print(f"<<<VENV_EXEC_START>>>{{output_json}}<<<VENV_EXEC_END>>>")
890
890
 
891
891
  # Execute the Python code in the virtual environment
892
892
  python_command = f"python -c \"import base64; exec(base64.b64decode('{encoded_code}').decode('utf-8'))\""
893
- stdout, stderr = await self.venv_cmd(venv_name, python_command)
893
+ result = await self.venv_cmd(venv_name, python_command)
894
894
 
895
895
  # Parse the output to extract the payload
896
896
  start_marker = "<<<VENV_EXEC_START>>>"
897
897
  end_marker = "<<<VENV_EXEC_END>>>"
898
898
 
899
899
  # Print original stdout
900
- print(stdout[:stdout.find(start_marker)])
900
+ print(result.stdout[:result.stdout.find(start_marker)])
901
901
 
902
- if start_marker in stdout and end_marker in stdout:
903
- start_idx = stdout.find(start_marker) + len(start_marker)
904
- end_idx = stdout.find(end_marker)
902
+ if start_marker in result.stdout and end_marker in result.stdout:
903
+ start_idx = result.stdout.find(start_marker) + len(start_marker)
904
+ end_idx = result.stdout.find(end_marker)
905
905
 
906
906
  if start_idx < end_idx:
907
- output_json = stdout[start_idx:end_idx]
907
+ output_json = result.stdout[start_idx:end_idx]
908
908
 
909
909
  try:
910
910
  # Decode and deserialize the output payload from JSON
@@ -923,4 +923,4 @@ print(f"<<<VENV_EXEC_START>>>{{output_json}}<<<VENV_EXEC_END>>>")
923
923
  raise Exception("Invalid output format: markers found but no content between them")
924
924
  else:
925
925
  # Fallback: return stdout/stderr if no payload markers found
926
- raise Exception(f"No output payload found. stdout: {stdout}, stderr: {stderr}")
926
+ raise Exception(f"No output payload found. stdout: {result.stdout}, stderr: {result.stderr}")
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple
5
5
  from PIL import Image
6
6
 
7
7
  import websockets
8
+ import aiohttp
8
9
 
9
10
  from ..logger import Logger, LogLevel
10
11
  from .base import BaseComputerInterface
@@ -57,6 +58,17 @@ class GenericComputerInterface(BaseComputerInterface):
57
58
  protocol = "wss" if self.api_key else "ws"
58
59
  port = "8443" if self.api_key else "8000"
59
60
  return f"{protocol}://{self.ip_address}:{port}/ws"
61
+
62
+ @property
63
+ def rest_uri(self) -> str:
64
+ """Get the REST URI using the current IP address.
65
+
66
+ Returns:
67
+ REST URI for the Computer API Server
68
+ """
69
+ protocol = "https" if self.api_key else "http"
70
+ port = "8443" if self.api_key else "8000"
71
+ return f"{protocol}://{self.ip_address}:{port}/cmd"
60
72
 
61
73
  # Mouse actions
62
74
  async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left", delay: Optional[float] = None) -> None:
@@ -677,7 +689,7 @@ class GenericComputerInterface(BaseComputerInterface):
677
689
 
678
690
  raise ConnectionError("Failed to establish WebSocket connection after multiple retries")
679
691
 
680
- async def _send_command(self, command: str, params: Optional[Dict] = None) -> Dict[str, Any]:
692
+ async def _send_command_ws(self, command: str, params: Optional[Dict] = None) -> Dict[str, Any]:
681
693
  """Send command through WebSocket."""
682
694
  max_retries = 3
683
695
  retry_count = 0
@@ -717,7 +729,151 @@ class GenericComputerInterface(BaseComputerInterface):
717
729
 
718
730
  raise last_error if last_error else RuntimeError("Failed to send command")
719
731
 
732
+ async def _send_command_rest(self, command: str, params: Optional[Dict] = None) -> Dict[str, Any]:
733
+ """Send command through REST API without retries or connection management."""
734
+ try:
735
+ # Prepare the request payload
736
+ payload = {"command": command, "params": params or {}}
737
+
738
+ # Prepare headers
739
+ headers = {"Content-Type": "application/json"}
740
+ if self.api_key:
741
+ headers["X-API-Key"] = self.api_key
742
+ if self.vm_name:
743
+ headers["X-Container-Name"] = self.vm_name
744
+
745
+ # Send the request
746
+ async with aiohttp.ClientSession() as session:
747
+ async with session.post(
748
+ self.rest_uri,
749
+ json=payload,
750
+ headers=headers
751
+ ) as response:
752
+ # Get the response text
753
+ response_text = await response.text()
754
+
755
+ # Trim whitespace
756
+ response_text = response_text.strip()
757
+
758
+ # Check if it starts with "data: "
759
+ if response_text.startswith("data: "):
760
+ # Extract everything after "data: "
761
+ json_str = response_text[6:] # Remove "data: " prefix
762
+ try:
763
+ return json.loads(json_str)
764
+ except json.JSONDecodeError:
765
+ return {
766
+ "success": False,
767
+ "error": "Server returned malformed response",
768
+ "message": response_text
769
+ }
770
+ else:
771
+ # Return error response
772
+ return {
773
+ "success": False,
774
+ "error": "Server returned malformed response",
775
+ "message": response_text
776
+ }
777
+
778
+ except Exception as e:
779
+ return {
780
+ "success": False,
781
+ "error": "Request failed",
782
+ "message": str(e)
783
+ }
784
+
785
+ async def _send_command(self, command: str, params: Optional[Dict] = None) -> Dict[str, Any]:
786
+ """Send command using REST API with WebSocket fallback."""
787
+ # Try REST API first
788
+ result = await self._send_command_rest(command, params)
789
+
790
+ # If REST failed with "Request failed", try WebSocket as fallback
791
+ if not result.get("success", True) and (result.get("error") == "Request failed" or result.get("error") == "Server returned malformed response"):
792
+ self.logger.debug(f"REST API failed for command '{command}', trying WebSocket fallback")
793
+ try:
794
+ return await self._send_command_ws(command, params)
795
+ except Exception as e:
796
+ self.logger.debug(f"WebSocket fallback also failed: {e}")
797
+ # Return the original REST error
798
+ return result
799
+
800
+ return result
801
+
720
802
  async def wait_for_ready(self, timeout: int = 60, interval: float = 1.0):
803
+ """Wait for Computer API Server to be ready by testing version command."""
804
+
805
+ # Check if REST API is available
806
+ try:
807
+ result = await self._send_command_rest("version", {})
808
+ assert result.get("success", True)
809
+ except Exception as e:
810
+ self.logger.debug(f"REST API failed for command 'version', trying WebSocket fallback: {e}")
811
+ try:
812
+ await self._wait_for_ready_ws(timeout, interval)
813
+ return
814
+ except Exception as e:
815
+ self.logger.debug(f"WebSocket fallback also failed: {e}")
816
+ raise e
817
+
818
+ start_time = time.time()
819
+ last_error = None
820
+ attempt_count = 0
821
+ progress_interval = 10 # Log progress every 10 seconds
822
+ last_progress_time = start_time
823
+
824
+ try:
825
+ self.logger.info(
826
+ f"Waiting for Computer API Server to be ready (timeout: {timeout}s)..."
827
+ )
828
+
829
+ # Wait for the server to respond to get_screen_size command
830
+ while time.time() - start_time < timeout:
831
+ try:
832
+ attempt_count += 1
833
+ current_time = time.time()
834
+
835
+ # Log progress periodically without flooding logs
836
+ if current_time - last_progress_time >= progress_interval:
837
+ elapsed = current_time - start_time
838
+ self.logger.info(
839
+ f"Still waiting for Computer API Server... (elapsed: {elapsed:.1f}s, attempts: {attempt_count})"
840
+ )
841
+ last_progress_time = current_time
842
+
843
+ # Test the server with a simple get_screen_size command
844
+ result = await self._send_command("get_screen_size")
845
+ if result.get("success", False):
846
+ elapsed = time.time() - start_time
847
+ self.logger.info(
848
+ f"Computer API Server is ready (after {elapsed:.1f}s, {attempt_count} attempts)"
849
+ )
850
+ return # Server is ready
851
+ else:
852
+ last_error = result.get("error", "Unknown error")
853
+ self.logger.debug(f"Initial connection command failed: {last_error}")
854
+
855
+ except Exception as e:
856
+ last_error = e
857
+ self.logger.debug(f"Connection attempt {attempt_count} failed: {e}")
858
+
859
+ # Wait before trying again
860
+ await asyncio.sleep(interval)
861
+
862
+ # If we get here, we've timed out
863
+ error_msg = f"Could not connect to {self.ip_address} after {timeout} seconds"
864
+ if last_error:
865
+ error_msg += f": {str(last_error)}"
866
+ self.logger.error(error_msg)
867
+ raise TimeoutError(error_msg)
868
+
869
+ except Exception as e:
870
+ if isinstance(e, TimeoutError):
871
+ raise
872
+ error_msg = f"Error while waiting for server: {str(e)}"
873
+ self.logger.error(error_msg)
874
+ raise RuntimeError(error_msg)
875
+
876
+ async def _wait_for_ready_ws(self, timeout: int = 60, interval: float = 1.0):
721
877
  """Wait for WebSocket connection to become available."""
722
878
  start_time = time.time()
723
879
  last_error = None
@@ -755,7 +911,7 @@ class GenericComputerInterface(BaseComputerInterface):
755
911
  if self._ws and self._ws.state == websockets.protocol.State.OPEN:
756
912
  # Test the connection with a simple command
757
913
  try:
758
- await self._send_command("get_screen_size")
914
+ await self._send_command_ws("get_screen_size")
759
915
  elapsed = time.time() - start_time
760
916
  self.logger.info(
761
917
  f"Computer API Server is ready (after {elapsed:.1f}s, {attempt_count} attempts)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cua-computer
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Computer-Use Interface (CUI) framework powering Cua
5
5
  Author-Email: TryCua <gh@trycua.com>
6
6
  Requires-Python: >=3.11
@@ -1,11 +1,11 @@
1
1
  computer/__init__.py,sha256=44ZBq815dMihgAHmBKn1S_GFNbElCXyZInh3hle1k9Y,1237
2
- computer/computer.py,sha256=bHo7pdJoz8p3YSERYvdY7aLYdqYdiXbPVQydirlhwkM,41390
2
+ computer/computer.py,sha256=uwbV0yZHhs5VUuMTGJvEaN8f7Xcr_msSQpufjdpZ2B4,41410
3
3
  computer/diorama_computer.py,sha256=jOP7_eXxxU6SMIoE25ni0YXPK0E7p5sZeLKmkYLh6G8,3871
4
4
  computer/helpers.py,sha256=iHkO2WhuCLc15g67kfMnpQWxfNRlz2YeJNEvYaL9jlM,1826
5
5
  computer/interface/__init__.py,sha256=xQvYjq5PMn9ZJOmRR5mWtONTl_0HVd8ACvW6AQnzDdw,262
6
6
  computer/interface/base.py,sha256=1beR4T0z5anb9NaNgKJrMJTF0BFIKyiHlokMLesOV5Q,15131
7
7
  computer/interface/factory.py,sha256=Eas5u9sOZ8FegwX51dP9M37oZBjy2EiVcmhTPc98L3Y,1639
8
- computer/interface/generic.py,sha256=EH9OCSU2PDG-9GAzIZdmzFfCgSAkPs1Pc8xfAQSnFAQ,36296
8
+ computer/interface/generic.py,sha256=LwesmF0NyZ9RWaDKZsXLt7UokQmbTK8sSGLhQ1yfLQU,43056
9
9
  computer/interface/linux.py,sha256=fDm2OwqfeeO72HwctboPEE5AwPTo2XBRDyYkwQxMyt0,417
10
10
  computer/interface/macos.py,sha256=m1aRn3BCbA95gPoO-WSP9NPwruT4BT5DZzxY10UuBI0,675
11
11
  computer/interface/models.py,sha256=kPpmoO-TSxSr95f5ELuTpobY-SckG1Sn9pE8zz1t008,3605
@@ -31,7 +31,7 @@ computer/ui/__main__.py,sha256=Jwy2oC_mGZLN0fX7WLqpjaQkbXMeM3ISrUc8WSRUG0c,284
31
31
  computer/ui/gradio/__init__.py,sha256=5_KimixM48-X74FCsLw7LbSt39MQfUMEL8-M9amK3Cw,117
32
32
  computer/ui/gradio/app.py,sha256=5_AG2dQR9RtFrGQNonScAw64rlswclKW26tYlFBdXtM,70396
33
33
  computer/utils.py,sha256=zY50NXB7r51GNLQ6l7lhG_qv0_ufpQ8n0-SDhCei8m4,2838
34
- cua_computer-0.3.4.dist-info/METADATA,sha256=QsaQuhlPQwQfEyofCsYtz_otdH0cjJrfmtxF5mlRTsE,5802
35
- cua_computer-0.3.4.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
36
- cua_computer-0.3.4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
37
- cua_computer-0.3.4.dist-info/RECORD,,
34
+ cua_computer-0.3.5.dist-info/METADATA,sha256=YXTgZWO99OCJTusRXd8R-UFpJjoFVgEB-eskOasojvM,5802
35
+ cua_computer-0.3.5.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
36
+ cua_computer-0.3.5.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
37
+ cua_computer-0.3.5.dist-info/RECORD,,