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 +12 -12
- computer/interface/generic.py +158 -2
- {cua_computer-0.3.4.dist-info → cua_computer-0.3.5.dist-info}/METADATA +1 -1
- {cua_computer-0.3.4.dist-info → cua_computer-0.3.5.dist-info}/RECORD +6 -6
- {cua_computer-0.3.4.dist-info → cua_computer-0.3.5.dist-info}/WHEEL +0 -0
- {cua_computer-0.3.4.dist-info → cua_computer-0.3.5.dist-info}/entry_points.txt +0 -0
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])
|
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
|
-
_
|
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)
|
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
|
-
|
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
|
-
|
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}")
|
computer/interface/generic.py
CHANGED
@@ -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
|
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.
|
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,11 +1,11 @@
|
|
1
1
|
computer/__init__.py,sha256=44ZBq815dMihgAHmBKn1S_GFNbElCXyZInh3hle1k9Y,1237
|
2
|
-
computer/computer.py,sha256=
|
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=
|
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.
|
35
|
-
cua_computer-0.3.
|
36
|
-
cua_computer-0.3.
|
37
|
-
cua_computer-0.3.
|
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,,
|
File without changes
|
File without changes
|