orgo 0.0.24__tar.gz → 0.0.26__tar.gz
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.
- {orgo-0.0.24 → orgo-0.0.26}/PKG-INFO +1 -1
- {orgo-0.0.24 → orgo-0.0.26}/pyproject.toml +1 -1
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo/__init__.py +2 -1
- orgo-0.0.26/src/orgo/api/client.py +191 -0
- orgo-0.0.26/src/orgo/computer.py +337 -0
- orgo-0.0.26/src/orgo/project.py +87 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo.egg-info/PKG-INFO +1 -1
- orgo-0.0.24/src/orgo/api/client.py +0 -165
- orgo-0.0.24/src/orgo/computer.py +0 -338
- orgo-0.0.24/src/orgo/project.py +0 -64
- {orgo-0.0.24 → orgo-0.0.26}/README.md +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/setup.cfg +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo/api/__init__.py +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo/prompt.py +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo/utils/__init__.py +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo/utils/auth.py +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo.egg-info/SOURCES.txt +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo.egg-info/dependency_links.txt +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo.egg-info/requires.txt +0 -0
- {orgo-0.0.24 → orgo-0.0.26}/src/orgo.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""API client for Orgo service"""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
|
|
6
|
+
from orgo.utils.auth import get_api_key
|
|
7
|
+
|
|
8
|
+
class ApiClient:
|
|
9
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
10
|
+
self.api_key = get_api_key(api_key)
|
|
11
|
+
self.base_url = base_url or "https://www.orgo.ai/api"
|
|
12
|
+
self.session = requests.Session()
|
|
13
|
+
self.session.headers.update({
|
|
14
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"Accept": "application/json"
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
def _request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20
|
+
url = f"{self.base_url}/{endpoint}"
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
if method.upper() == "GET":
|
|
24
|
+
response = self.session.get(url, params=data)
|
|
25
|
+
else:
|
|
26
|
+
response = self.session.request(method, url, json=data)
|
|
27
|
+
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
return response.json()
|
|
30
|
+
except requests.exceptions.RequestException as e:
|
|
31
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
32
|
+
error_message = f"API error: {e.response.status_code}"
|
|
33
|
+
try:
|
|
34
|
+
error_data = e.response.json()
|
|
35
|
+
if 'error' in error_data:
|
|
36
|
+
error_message += f" - {error_data['error']}"
|
|
37
|
+
except ValueError:
|
|
38
|
+
pass
|
|
39
|
+
raise Exception(error_message) from e
|
|
40
|
+
raise Exception(f"Connection error: {str(e)}") from e
|
|
41
|
+
|
|
42
|
+
# Project methods
|
|
43
|
+
def create_project(self, name: str) -> Dict[str, Any]:
|
|
44
|
+
"""Create a new named project"""
|
|
45
|
+
return self._request("POST", "projects", {"name": name})
|
|
46
|
+
|
|
47
|
+
def get_project_by_name(self, name: str) -> Dict[str, Any]:
|
|
48
|
+
"""Get project details by name"""
|
|
49
|
+
return self._request("GET", f"projects/by-name/{name}")
|
|
50
|
+
|
|
51
|
+
def get_project(self, project_id: str) -> Dict[str, Any]:
|
|
52
|
+
"""Get project details by ID"""
|
|
53
|
+
return self._request("GET", f"projects/{project_id}")
|
|
54
|
+
|
|
55
|
+
def list_projects(self) -> List[Dict[str, Any]]:
|
|
56
|
+
"""List all projects"""
|
|
57
|
+
response = self._request("GET", "projects")
|
|
58
|
+
return response.get("projects", [])
|
|
59
|
+
|
|
60
|
+
def start_project(self, project_id: str) -> Dict[str, Any]:
|
|
61
|
+
"""Start a project"""
|
|
62
|
+
return self._request("POST", f"projects/{project_id}/start")
|
|
63
|
+
|
|
64
|
+
def stop_project(self, project_id: str) -> Dict[str, Any]:
|
|
65
|
+
"""Stop a project"""
|
|
66
|
+
return self._request("POST", f"projects/{project_id}/stop")
|
|
67
|
+
|
|
68
|
+
def restart_project(self, project_id: str) -> Dict[str, Any]:
|
|
69
|
+
"""Restart a project"""
|
|
70
|
+
return self._request("POST", f"projects/{project_id}/restart")
|
|
71
|
+
|
|
72
|
+
def delete_project(self, project_id: str) -> Dict[str, Any]:
|
|
73
|
+
"""Delete a project and all its computers"""
|
|
74
|
+
return self._request("POST", f"projects/{project_id}/delete")
|
|
75
|
+
|
|
76
|
+
# Computer methods
|
|
77
|
+
def create_computer(self, project_name: str, computer_name: str,
|
|
78
|
+
os: str = "linux", ram: int = 2, cpu: int = 2,
|
|
79
|
+
gpu: str = "none") -> Dict[str, Any]:
|
|
80
|
+
"""Create a new computer within a project"""
|
|
81
|
+
return self._request("POST", f"projects/{project_name}/computers", {
|
|
82
|
+
"name": computer_name,
|
|
83
|
+
"os": os,
|
|
84
|
+
"ram": ram,
|
|
85
|
+
"cpu": cpu,
|
|
86
|
+
"gpu": gpu
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
def list_computers(self, project_name: str) -> List[Dict[str, Any]]:
|
|
90
|
+
"""List all computers in a project"""
|
|
91
|
+
response = self._request("GET", f"projects/{project_name}/computers")
|
|
92
|
+
return response.get("computers", [])
|
|
93
|
+
|
|
94
|
+
def get_computer(self, computer_id: str) -> Dict[str, Any]:
|
|
95
|
+
"""Get computer details"""
|
|
96
|
+
return self._request("GET", f"computers/{computer_id}")
|
|
97
|
+
|
|
98
|
+
def delete_computer(self, computer_id: str) -> Dict[str, Any]:
|
|
99
|
+
"""Delete a computer"""
|
|
100
|
+
return self._request("DELETE", f"computers/{computer_id}")
|
|
101
|
+
|
|
102
|
+
def start_computer(self, computer_id: str) -> Dict[str, Any]:
|
|
103
|
+
"""Start a computer"""
|
|
104
|
+
return self._request("POST", f"computers/{computer_id}/start")
|
|
105
|
+
|
|
106
|
+
def stop_computer(self, computer_id: str) -> Dict[str, Any]:
|
|
107
|
+
"""Stop a computer"""
|
|
108
|
+
return self._request("POST", f"computers/{computer_id}/stop")
|
|
109
|
+
|
|
110
|
+
def restart_computer(self, computer_id: str) -> Dict[str, Any]:
|
|
111
|
+
"""Restart a computer"""
|
|
112
|
+
return self._request("POST", f"computers/{computer_id}/restart")
|
|
113
|
+
|
|
114
|
+
# Computer control methods
|
|
115
|
+
def left_click(self, computer_id: str, x: int, y: int) -> Dict[str, Any]:
|
|
116
|
+
return self._request("POST", f"computers/{computer_id}/click", {
|
|
117
|
+
"button": "left", "x": x, "y": y
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
def right_click(self, computer_id: str, x: int, y: int) -> Dict[str, Any]:
|
|
121
|
+
return self._request("POST", f"computers/{computer_id}/click", {
|
|
122
|
+
"button": "right", "x": x, "y": y
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
def double_click(self, computer_id: str, x: int, y: int) -> Dict[str, Any]:
|
|
126
|
+
return self._request("POST", f"computers/{computer_id}/click", {
|
|
127
|
+
"button": "left", "x": x, "y": y, "double": True
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
def drag(self, computer_id: str, start_x: int, start_y: int,
|
|
131
|
+
end_x: int, end_y: int, button: str = "left",
|
|
132
|
+
duration: float = 0.5) -> Dict[str, Any]:
|
|
133
|
+
"""Perform a drag operation from start to end coordinates"""
|
|
134
|
+
return self._request("POST", f"computers/{computer_id}/drag", {
|
|
135
|
+
"start_x": start_x,
|
|
136
|
+
"start_y": start_y,
|
|
137
|
+
"end_x": end_x,
|
|
138
|
+
"end_y": end_y,
|
|
139
|
+
"button": button,
|
|
140
|
+
"duration": duration
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
def scroll(self, computer_id: str, direction: str, amount: int = 3) -> Dict[str, Any]:
|
|
144
|
+
return self._request("POST", f"computers/{computer_id}/scroll", {
|
|
145
|
+
"direction": direction, "amount": amount
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
def type_text(self, computer_id: str, text: str) -> Dict[str, Any]:
|
|
149
|
+
return self._request("POST", f"computers/{computer_id}/type", {
|
|
150
|
+
"text": text
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
def key_press(self, computer_id: str, key: str) -> Dict[str, Any]:
|
|
154
|
+
return self._request("POST", f"computers/{computer_id}/key", {
|
|
155
|
+
"key": key
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
def get_screenshot(self, computer_id: str) -> Dict[str, Any]:
|
|
159
|
+
return self._request("GET", f"computers/{computer_id}/screenshot")
|
|
160
|
+
|
|
161
|
+
def execute_bash(self, computer_id: str, command: str) -> Dict[str, Any]:
|
|
162
|
+
return self._request("POST", f"computers/{computer_id}/bash", {
|
|
163
|
+
"command": command
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
def execute_python(self, computer_id: str, code: str, timeout: int = 10) -> Dict[str, Any]:
|
|
167
|
+
"""Execute Python code on the computer"""
|
|
168
|
+
return self._request("POST", f"computers/{computer_id}/exec", {
|
|
169
|
+
"code": code,
|
|
170
|
+
"timeout": timeout
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
def wait(self, computer_id: str, duration: float) -> Dict[str, Any]:
|
|
174
|
+
return self._request("POST", f"computers/{computer_id}/wait", {
|
|
175
|
+
"duration": duration
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
# Streaming methods
|
|
179
|
+
def start_stream(self, computer_id: str, connection_name: str) -> Dict[str, Any]:
|
|
180
|
+
"""Start streaming to a configured RTMP connection"""
|
|
181
|
+
return self._request("POST", f"computers/{computer_id}/stream/start", {
|
|
182
|
+
"connection_name": connection_name
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
def stop_stream(self, computer_id: str) -> Dict[str, Any]:
|
|
186
|
+
"""Stop the active stream"""
|
|
187
|
+
return self._request("POST", f"computers/{computer_id}/stream/stop")
|
|
188
|
+
|
|
189
|
+
def get_stream_status(self, computer_id: str) -> Dict[str, Any]:
|
|
190
|
+
"""Get current stream status"""
|
|
191
|
+
return self._request("GET", f"computers/{computer_id}/stream/status")
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Computer class for interacting with Orgo virtual environments"""
|
|
2
|
+
import os
|
|
3
|
+
import io
|
|
4
|
+
import base64
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Dict, List, Any, Optional, Callable, Literal, Union
|
|
8
|
+
from PIL import Image
|
|
9
|
+
import requests
|
|
10
|
+
from requests.exceptions import RequestException
|
|
11
|
+
|
|
12
|
+
from .api.client import ApiClient
|
|
13
|
+
from .prompt import get_provider
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class Computer:
|
|
18
|
+
def __init__(self,
|
|
19
|
+
project: Optional[Union[str, 'Project']] = None,
|
|
20
|
+
name: Optional[str] = None,
|
|
21
|
+
computer_id: Optional[str] = None,
|
|
22
|
+
api_key: Optional[str] = None,
|
|
23
|
+
base_api_url: Optional[str] = None,
|
|
24
|
+
ram: Optional[Literal[1, 2, 4, 8, 16, 32, 64]] = None,
|
|
25
|
+
memory: Optional[Literal[1, 2, 4, 8, 16, 32, 64]] = None,
|
|
26
|
+
cpu: Optional[Literal[1, 2, 4, 8, 16]] = None,
|
|
27
|
+
os: Optional[Literal["linux", "windows"]] = None,
|
|
28
|
+
gpu: Optional[Literal["none", "a10", "l40s", "a100-40gb", "a100-80gb"]] = None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize an Orgo virtual computer.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project: Project name (str) or Project instance. If not provided, creates a new project.
|
|
34
|
+
name: Computer name within the project (optional, auto-generated if not provided)
|
|
35
|
+
computer_id: Existing computer ID to connect to (optional)
|
|
36
|
+
api_key: Orgo API key (defaults to ORGO_API_KEY env var)
|
|
37
|
+
base_api_url: Custom API URL (optional)
|
|
38
|
+
ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, or 64) - only used when creating
|
|
39
|
+
cpu: CPU cores (1, 2, 4, 8, or 16) - only used when creating
|
|
40
|
+
os: Operating system ("linux" or "windows") - only used when creating
|
|
41
|
+
gpu: GPU type - only used when creating
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
# Create computer in new project
|
|
45
|
+
computer = Computer(ram=4, cpu=2)
|
|
46
|
+
|
|
47
|
+
# Create computer in existing project
|
|
48
|
+
computer = Computer(project="manus", ram=4, cpu=2)
|
|
49
|
+
|
|
50
|
+
# Connect to existing computer in project
|
|
51
|
+
computer = Computer(project="manus")
|
|
52
|
+
"""
|
|
53
|
+
self.api_key = api_key or os.environ.get("ORGO_API_KEY")
|
|
54
|
+
self.base_api_url = base_api_url
|
|
55
|
+
self.api = ApiClient(self.api_key, self.base_api_url)
|
|
56
|
+
|
|
57
|
+
# Handle memory parameter as an alias for ram
|
|
58
|
+
if ram is None and memory is not None:
|
|
59
|
+
ram = memory
|
|
60
|
+
|
|
61
|
+
# Store configuration
|
|
62
|
+
self.os = os or "linux"
|
|
63
|
+
self.ram = ram or 2
|
|
64
|
+
self.cpu = cpu or 2
|
|
65
|
+
self.gpu = gpu or "none"
|
|
66
|
+
|
|
67
|
+
if computer_id:
|
|
68
|
+
# Connect to existing computer by ID
|
|
69
|
+
self._connect_by_id(computer_id)
|
|
70
|
+
elif project:
|
|
71
|
+
# Work with specified project
|
|
72
|
+
if isinstance(project, str):
|
|
73
|
+
# Project name provided
|
|
74
|
+
self.project_name = project
|
|
75
|
+
self._initialize_with_project_name(project, name)
|
|
76
|
+
else:
|
|
77
|
+
# Project instance provided
|
|
78
|
+
from .project import Project as ProjectClass
|
|
79
|
+
if isinstance(project, ProjectClass):
|
|
80
|
+
self.project_name = project.name
|
|
81
|
+
self.project_id = project.id
|
|
82
|
+
self._initialize_with_project_instance(project, name)
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError("project must be a string (project name) or Project instance")
|
|
85
|
+
else:
|
|
86
|
+
# No project specified, create a new one
|
|
87
|
+
self._create_new_project_and_computer(name)
|
|
88
|
+
|
|
89
|
+
def _connect_by_id(self, computer_id: str):
|
|
90
|
+
"""Connect to existing computer by ID"""
|
|
91
|
+
self.computer_id = computer_id
|
|
92
|
+
self._info = self.api.get_computer(computer_id)
|
|
93
|
+
self.project_name = self._info.get("project_name")
|
|
94
|
+
self.name = self._info.get("name")
|
|
95
|
+
|
|
96
|
+
def _initialize_with_project_name(self, project_name: str, computer_name: Optional[str]):
|
|
97
|
+
"""Initialize with a project name (create project if needed)"""
|
|
98
|
+
try:
|
|
99
|
+
# Try to get existing project
|
|
100
|
+
project = self.api.get_project_by_name(project_name)
|
|
101
|
+
self.project_id = project.get("id")
|
|
102
|
+
|
|
103
|
+
# Check for existing computers
|
|
104
|
+
computers = self.api.list_computers(project_name)
|
|
105
|
+
|
|
106
|
+
if computer_name:
|
|
107
|
+
# Look for specific computer
|
|
108
|
+
existing = next((c for c in computers if c.get("name") == computer_name), None)
|
|
109
|
+
if existing:
|
|
110
|
+
self._connect_to_existing_computer(existing)
|
|
111
|
+
else:
|
|
112
|
+
# Create new computer with specified name
|
|
113
|
+
self._create_computer(project_name, computer_name)
|
|
114
|
+
elif computers:
|
|
115
|
+
# No name specified, use first available computer
|
|
116
|
+
self._connect_to_existing_computer(computers[0])
|
|
117
|
+
else:
|
|
118
|
+
# No computers exist, create new one
|
|
119
|
+
self._create_computer(project_name, computer_name)
|
|
120
|
+
|
|
121
|
+
except Exception:
|
|
122
|
+
# Project doesn't exist, create it
|
|
123
|
+
logger.info(f"Project {project_name} not found, creating new project")
|
|
124
|
+
project = self.api.create_project(project_name)
|
|
125
|
+
self.project_id = project.get("id")
|
|
126
|
+
self._create_computer(project_name, computer_name)
|
|
127
|
+
|
|
128
|
+
def _initialize_with_project_instance(self, project: 'Project', computer_name: Optional[str]):
|
|
129
|
+
"""Initialize with a Project instance"""
|
|
130
|
+
computers = project.list_computers()
|
|
131
|
+
|
|
132
|
+
if computer_name:
|
|
133
|
+
# Look for specific computer
|
|
134
|
+
existing = next((c for c in computers if c.get("name") == computer_name), None)
|
|
135
|
+
if existing:
|
|
136
|
+
self._connect_to_existing_computer(existing)
|
|
137
|
+
else:
|
|
138
|
+
# Create new computer with specified name
|
|
139
|
+
self._create_computer(project.name, computer_name)
|
|
140
|
+
elif computers:
|
|
141
|
+
# No name specified, use first available computer
|
|
142
|
+
self._connect_to_existing_computer(computers[0])
|
|
143
|
+
else:
|
|
144
|
+
# No computers exist, create new one
|
|
145
|
+
self._create_computer(project.name, computer_name)
|
|
146
|
+
|
|
147
|
+
def _create_new_project_and_computer(self, computer_name: Optional[str]):
|
|
148
|
+
"""Create a new project and computer"""
|
|
149
|
+
# Generate a unique project name
|
|
150
|
+
project_name = f"project-{uuid.uuid4().hex[:8]}"
|
|
151
|
+
|
|
152
|
+
# Create the project
|
|
153
|
+
project = self.api.create_project(project_name)
|
|
154
|
+
self.project_id = project.get("id")
|
|
155
|
+
self.project_name = project_name
|
|
156
|
+
|
|
157
|
+
# Create a computer in the new project
|
|
158
|
+
self._create_computer(project_name, computer_name)
|
|
159
|
+
|
|
160
|
+
def _connect_to_existing_computer(self, computer_info: Dict[str, Any]):
|
|
161
|
+
"""Connect to an existing computer"""
|
|
162
|
+
self.computer_id = computer_info.get("id")
|
|
163
|
+
self.name = computer_info.get("name")
|
|
164
|
+
self._info = computer_info
|
|
165
|
+
logger.info(f"Connected to existing computer {self.name} (ID: {self.computer_id})")
|
|
166
|
+
|
|
167
|
+
def _create_computer(self, project_name: str, computer_name: Optional[str]):
|
|
168
|
+
"""Create a new computer in the project"""
|
|
169
|
+
# Generate name if not provided
|
|
170
|
+
if not computer_name:
|
|
171
|
+
computer_name = f"desktop-{uuid.uuid4().hex[:8]}"
|
|
172
|
+
|
|
173
|
+
self.name = computer_name
|
|
174
|
+
|
|
175
|
+
# Validate parameters
|
|
176
|
+
if self.ram not in [1, 2, 4, 8, 16, 32, 64]:
|
|
177
|
+
raise ValueError("ram must be one of: 1, 2, 4, 8, 16, 32, 64 GB")
|
|
178
|
+
if self.cpu not in [1, 2, 4, 8, 16]:
|
|
179
|
+
raise ValueError("cpu must be one of: 1, 2, 4, 8, 16 cores")
|
|
180
|
+
if self.os not in ["linux", "windows"]:
|
|
181
|
+
raise ValueError("os must be either 'linux' or 'windows'")
|
|
182
|
+
if self.gpu not in ["none", "a10", "l40s", "a100-40gb", "a100-80gb"]:
|
|
183
|
+
raise ValueError("gpu must be one of: 'none', 'a10', 'l40s', 'a100-40gb', 'a100-80gb'")
|
|
184
|
+
|
|
185
|
+
computer = self.api.create_computer(
|
|
186
|
+
project_name=project_name,
|
|
187
|
+
computer_name=computer_name,
|
|
188
|
+
os=self.os,
|
|
189
|
+
ram=self.ram,
|
|
190
|
+
cpu=self.cpu,
|
|
191
|
+
gpu=self.gpu
|
|
192
|
+
)
|
|
193
|
+
self.computer_id = computer.get("id")
|
|
194
|
+
self._info = computer
|
|
195
|
+
logger.info(f"Created new computer {self.name} (ID: {self.computer_id})")
|
|
196
|
+
|
|
197
|
+
def status(self) -> Dict[str, Any]:
|
|
198
|
+
"""Get current computer status"""
|
|
199
|
+
return self.api.get_computer(self.computer_id)
|
|
200
|
+
|
|
201
|
+
def start(self) -> Dict[str, Any]:
|
|
202
|
+
"""Start the computer"""
|
|
203
|
+
return self.api.start_computer(self.computer_id)
|
|
204
|
+
|
|
205
|
+
def stop(self) -> Dict[str, Any]:
|
|
206
|
+
"""Stop the computer"""
|
|
207
|
+
return self.api.stop_computer(self.computer_id)
|
|
208
|
+
|
|
209
|
+
def restart(self) -> Dict[str, Any]:
|
|
210
|
+
"""Restart the computer"""
|
|
211
|
+
return self.api.restart_computer(self.computer_id)
|
|
212
|
+
|
|
213
|
+
def destroy(self) -> Dict[str, Any]:
|
|
214
|
+
"""Terminate and delete the computer instance"""
|
|
215
|
+
return self.api.delete_computer(self.computer_id)
|
|
216
|
+
|
|
217
|
+
# Navigation methods
|
|
218
|
+
def left_click(self, x: int, y: int) -> Dict[str, Any]:
|
|
219
|
+
"""Perform left mouse click at specified coordinates"""
|
|
220
|
+
return self.api.left_click(self.computer_id, x, y)
|
|
221
|
+
|
|
222
|
+
def right_click(self, x: int, y: int) -> Dict[str, Any]:
|
|
223
|
+
"""Perform right mouse click at specified coordinates"""
|
|
224
|
+
return self.api.right_click(self.computer_id, x, y)
|
|
225
|
+
|
|
226
|
+
def double_click(self, x: int, y: int) -> Dict[str, Any]:
|
|
227
|
+
"""Perform double click at specified coordinates"""
|
|
228
|
+
return self.api.double_click(self.computer_id, x, y)
|
|
229
|
+
|
|
230
|
+
def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
|
231
|
+
button: str = "left", duration: float = 0.5) -> Dict[str, Any]:
|
|
232
|
+
"""Perform a smooth drag operation from start to end coordinates"""
|
|
233
|
+
return self.api.drag(self.computer_id, start_x, start_y, end_x, end_y, button, duration)
|
|
234
|
+
|
|
235
|
+
def scroll(self, direction: str = "down", amount: int = 3) -> Dict[str, Any]:
|
|
236
|
+
"""Scroll in specified direction and amount"""
|
|
237
|
+
return self.api.scroll(self.computer_id, direction, amount)
|
|
238
|
+
|
|
239
|
+
# Input methods
|
|
240
|
+
def type(self, text: str) -> Dict[str, Any]:
|
|
241
|
+
"""Type the specified text"""
|
|
242
|
+
return self.api.type_text(self.computer_id, text)
|
|
243
|
+
|
|
244
|
+
def key(self, key: str) -> Dict[str, Any]:
|
|
245
|
+
"""Press a key or key combination (e.g., "Enter", "ctrl+c")"""
|
|
246
|
+
return self.api.key_press(self.computer_id, key)
|
|
247
|
+
|
|
248
|
+
# View methods
|
|
249
|
+
def screenshot(self) -> Image.Image:
|
|
250
|
+
"""Capture screenshot and return as PIL Image"""
|
|
251
|
+
response = self.api.get_screenshot(self.computer_id)
|
|
252
|
+
image_data = response.get("image", "")
|
|
253
|
+
|
|
254
|
+
if image_data.startswith(('http://', 'https://')):
|
|
255
|
+
img_response = requests.get(image_data)
|
|
256
|
+
img_response.raise_for_status()
|
|
257
|
+
return Image.open(io.BytesIO(img_response.content))
|
|
258
|
+
else:
|
|
259
|
+
img_data = base64.b64decode(image_data)
|
|
260
|
+
return Image.open(io.BytesIO(img_data))
|
|
261
|
+
|
|
262
|
+
def screenshot_base64(self) -> str:
|
|
263
|
+
"""Capture screenshot and return as base64 string"""
|
|
264
|
+
response = self.api.get_screenshot(self.computer_id)
|
|
265
|
+
image_data = response.get("image", "")
|
|
266
|
+
|
|
267
|
+
if image_data.startswith(('http://', 'https://')):
|
|
268
|
+
img_response = requests.get(image_data)
|
|
269
|
+
img_response.raise_for_status()
|
|
270
|
+
return base64.b64encode(img_response.content).decode('utf-8')
|
|
271
|
+
else:
|
|
272
|
+
return image_data
|
|
273
|
+
|
|
274
|
+
# Execution methods
|
|
275
|
+
def bash(self, command: str) -> str:
|
|
276
|
+
"""Execute a bash command and return output"""
|
|
277
|
+
response = self.api.execute_bash(self.computer_id, command)
|
|
278
|
+
return response.get("output", "")
|
|
279
|
+
|
|
280
|
+
def exec(self, code: str, timeout: int = 10) -> Dict[str, Any]:
|
|
281
|
+
"""Execute Python code on the remote computer"""
|
|
282
|
+
response = self.api.execute_python(self.computer_id, code, timeout)
|
|
283
|
+
return response
|
|
284
|
+
|
|
285
|
+
def wait(self, seconds: float) -> Dict[str, Any]:
|
|
286
|
+
"""Wait for specified number of seconds"""
|
|
287
|
+
return self.api.wait(self.computer_id, seconds)
|
|
288
|
+
|
|
289
|
+
# Streaming methods
|
|
290
|
+
def start_stream(self, connection: str) -> Dict[str, Any]:
|
|
291
|
+
"""Start streaming the computer screen to an RTMP server"""
|
|
292
|
+
return self.api.start_stream(self.computer_id, connection)
|
|
293
|
+
|
|
294
|
+
def stop_stream(self) -> Dict[str, Any]:
|
|
295
|
+
"""Stop the active stream"""
|
|
296
|
+
return self.api.stop_stream(self.computer_id)
|
|
297
|
+
|
|
298
|
+
def stream_status(self) -> Dict[str, Any]:
|
|
299
|
+
"""Get the current streaming status"""
|
|
300
|
+
return self.api.get_stream_status(self.computer_id)
|
|
301
|
+
|
|
302
|
+
# AI control method
|
|
303
|
+
def prompt(self,
|
|
304
|
+
instruction: str,
|
|
305
|
+
provider: str = "anthropic",
|
|
306
|
+
model: str = "claude-3-7-sonnet-20250219",
|
|
307
|
+
display_width: int = 1024,
|
|
308
|
+
display_height: int = 768,
|
|
309
|
+
callback: Optional[Callable[[str, Any], None]] = None,
|
|
310
|
+
thinking_enabled: bool = False,
|
|
311
|
+
thinking_budget: int = 1024,
|
|
312
|
+
max_tokens: int = 4096,
|
|
313
|
+
max_iterations: int = 20,
|
|
314
|
+
max_saved_screenshots: int = 5,
|
|
315
|
+
api_key: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
316
|
+
"""Control the computer with natural language instructions using an AI assistant"""
|
|
317
|
+
provider_instance = get_provider(provider)
|
|
318
|
+
|
|
319
|
+
return provider_instance.execute(
|
|
320
|
+
computer_id=self.computer_id,
|
|
321
|
+
instruction=instruction,
|
|
322
|
+
callback=callback,
|
|
323
|
+
api_key=api_key,
|
|
324
|
+
model=model,
|
|
325
|
+
display_width=display_width,
|
|
326
|
+
display_height=display_height,
|
|
327
|
+
thinking_enabled=thinking_enabled,
|
|
328
|
+
thinking_budget=thinking_budget,
|
|
329
|
+
max_tokens=max_tokens,
|
|
330
|
+
max_iterations=max_iterations,
|
|
331
|
+
max_saved_screenshots=max_saved_screenshots,
|
|
332
|
+
orgo_api_key=self.api_key,
|
|
333
|
+
orgo_base_url=self.base_api_url
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def __repr__(self):
|
|
337
|
+
return f"Computer(name='{self.name}', project='{self.project_name}', id='{self.computer_id}')"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Project class for managing Orgo projects"""
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Dict, List, Any, Optional
|
|
5
|
+
|
|
6
|
+
from .api.client import ApiClient
|
|
7
|
+
|
|
8
|
+
class Project:
|
|
9
|
+
def __init__(self,
|
|
10
|
+
name: Optional[str] = None,
|
|
11
|
+
api_key: Optional[str] = None,
|
|
12
|
+
base_api_url: Optional[str] = None):
|
|
13
|
+
"""
|
|
14
|
+
Initialize an Orgo project.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name: Project name. If exists, connects to it. If not, creates it.
|
|
18
|
+
api_key: Orgo API key (defaults to ORGO_API_KEY env var)
|
|
19
|
+
base_api_url: Custom API URL (optional)
|
|
20
|
+
"""
|
|
21
|
+
self.api_key = api_key or os.environ.get("ORGO_API_KEY")
|
|
22
|
+
self.base_api_url = base_api_url
|
|
23
|
+
self.api = ApiClient(self.api_key, self.base_api_url)
|
|
24
|
+
|
|
25
|
+
if name:
|
|
26
|
+
self.name = name
|
|
27
|
+
else:
|
|
28
|
+
# Generate a unique name if not provided
|
|
29
|
+
self.name = f"project-{uuid.uuid4().hex[:8]}"
|
|
30
|
+
|
|
31
|
+
# Try to get existing project or create new one
|
|
32
|
+
self._initialize_project()
|
|
33
|
+
|
|
34
|
+
def _initialize_project(self):
|
|
35
|
+
"""Get existing project or create new one"""
|
|
36
|
+
try:
|
|
37
|
+
# Try to get existing project
|
|
38
|
+
project = self.api.get_project_by_name(self.name)
|
|
39
|
+
self.id = project.get("id")
|
|
40
|
+
self._info = project
|
|
41
|
+
except Exception:
|
|
42
|
+
# Project doesn't exist, create it
|
|
43
|
+
project = self.api.create_project(self.name)
|
|
44
|
+
self.id = project.get("id")
|
|
45
|
+
self._info = project
|
|
46
|
+
|
|
47
|
+
def status(self) -> Dict[str, Any]:
|
|
48
|
+
"""Get project status"""
|
|
49
|
+
return self.api.get_project(self.id)
|
|
50
|
+
|
|
51
|
+
def start(self) -> Dict[str, Any]:
|
|
52
|
+
"""Start all computers in the project"""
|
|
53
|
+
return self.api.start_project(self.id)
|
|
54
|
+
|
|
55
|
+
def stop(self) -> Dict[str, Any]:
|
|
56
|
+
"""Stop all computers in the project"""
|
|
57
|
+
return self.api.stop_project(self.id)
|
|
58
|
+
|
|
59
|
+
def restart(self) -> Dict[str, Any]:
|
|
60
|
+
"""Restart all computers in the project"""
|
|
61
|
+
return self.api.restart_project(self.id)
|
|
62
|
+
|
|
63
|
+
def destroy(self) -> Dict[str, Any]:
|
|
64
|
+
"""Delete the project and all its computers"""
|
|
65
|
+
return self.api.delete_project(self.id)
|
|
66
|
+
|
|
67
|
+
def list_computers(self) -> List[Dict[str, Any]]:
|
|
68
|
+
"""List all computers in this project"""
|
|
69
|
+
return self.api.list_computers(self.name)
|
|
70
|
+
|
|
71
|
+
def get_computer(self, computer_name: str = None) -> Optional[Dict[str, Any]]:
|
|
72
|
+
"""Get a specific computer in this project by name, or the first one if no name specified"""
|
|
73
|
+
computers = self.list_computers()
|
|
74
|
+
if not computers:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if computer_name:
|
|
78
|
+
for computer in computers:
|
|
79
|
+
if computer.get("name") == computer_name:
|
|
80
|
+
return computer
|
|
81
|
+
return None
|
|
82
|
+
else:
|
|
83
|
+
# Return first computer if no name specified
|
|
84
|
+
return computers[0]
|
|
85
|
+
|
|
86
|
+
def __repr__(self):
|
|
87
|
+
return f"Project(name='{self.name}', id='{self.id}')"
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
"""API client for Orgo service"""
|
|
2
|
-
|
|
3
|
-
import requests
|
|
4
|
-
from typing import Dict, Any, Optional
|
|
5
|
-
|
|
6
|
-
from orgo.utils.auth import get_api_key
|
|
7
|
-
|
|
8
|
-
class ApiClient:
|
|
9
|
-
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
10
|
-
self.api_key = get_api_key(api_key)
|
|
11
|
-
self.base_url = base_url or "https://www.orgo.ai/api"
|
|
12
|
-
self.session = requests.Session()
|
|
13
|
-
self.session.headers.update({
|
|
14
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
15
|
-
"Content-Type": "application/json",
|
|
16
|
-
"Accept": "application/json"
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
def _request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20
|
-
url = f"{self.base_url}/{endpoint}"
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
if method.upper() == "GET":
|
|
24
|
-
response = self.session.get(url, params=data)
|
|
25
|
-
else:
|
|
26
|
-
response = self.session.request(method, url, json=data)
|
|
27
|
-
|
|
28
|
-
response.raise_for_status()
|
|
29
|
-
return response.json()
|
|
30
|
-
except requests.exceptions.RequestException as e:
|
|
31
|
-
if hasattr(e, 'response') and e.response is not None:
|
|
32
|
-
error_message = f"API error: {e.response.status_code}"
|
|
33
|
-
try:
|
|
34
|
-
error_data = e.response.json()
|
|
35
|
-
if 'error' in error_data:
|
|
36
|
-
error_message += f" - {error_data['error']}"
|
|
37
|
-
except ValueError:
|
|
38
|
-
pass
|
|
39
|
-
raise Exception(error_message) from e
|
|
40
|
-
raise Exception(f"Connection error: {str(e)}") from e
|
|
41
|
-
|
|
42
|
-
# Computer lifecycle methods
|
|
43
|
-
def create_computer(self, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
44
|
-
"""Create a new project with desktop instance"""
|
|
45
|
-
payload = {}
|
|
46
|
-
if config:
|
|
47
|
-
payload["config"] = config
|
|
48
|
-
return self._request("POST", "projects", payload if payload else None)
|
|
49
|
-
|
|
50
|
-
def connect_computer(self, project_id: str) -> Dict[str, Any]:
|
|
51
|
-
return self._request("GET", f"projects/by-name/{project_id}")
|
|
52
|
-
|
|
53
|
-
def get_status(self, project_id: str) -> Dict[str, Any]:
|
|
54
|
-
return self._request("GET", f"projects/by-name/{project_id}")
|
|
55
|
-
|
|
56
|
-
def start_computer(self, project_name: str) -> Dict[str, Any]:
|
|
57
|
-
# Get the actual project ID from the name
|
|
58
|
-
project = self.get_status(project_name)
|
|
59
|
-
project_id = project.get("id")
|
|
60
|
-
if not project_id:
|
|
61
|
-
raise ValueError(f"Could not find ID for project {project_name}")
|
|
62
|
-
return self._request("POST", f"projects/{project_id}/start")
|
|
63
|
-
|
|
64
|
-
def stop_computer(self, project_name: str) -> Dict[str, Any]:
|
|
65
|
-
# Get the actual project ID from the name
|
|
66
|
-
project = self.get_status(project_name)
|
|
67
|
-
project_id = project.get("id")
|
|
68
|
-
if not project_id:
|
|
69
|
-
raise ValueError(f"Could not find ID for project {project_name}")
|
|
70
|
-
return self._request("POST", f"projects/{project_id}/stop")
|
|
71
|
-
|
|
72
|
-
def restart_computer(self, project_name: str) -> Dict[str, Any]:
|
|
73
|
-
# Get the actual project ID from the name
|
|
74
|
-
project = self.get_status(project_name)
|
|
75
|
-
project_id = project.get("id")
|
|
76
|
-
if not project_id:
|
|
77
|
-
raise ValueError(f"Could not find ID for project {project_name}")
|
|
78
|
-
return self._request("POST", f"projects/{project_id}/restart")
|
|
79
|
-
|
|
80
|
-
def delete_computer(self, project_name: str) -> Dict[str, Any]:
|
|
81
|
-
# Get the actual project ID from the name
|
|
82
|
-
project = self.get_status(project_name)
|
|
83
|
-
project_id = project.get("id")
|
|
84
|
-
if not project_id:
|
|
85
|
-
raise ValueError(f"Could not find ID for project {project_name}")
|
|
86
|
-
return self._request("POST", f"projects/{project_id}/delete")
|
|
87
|
-
|
|
88
|
-
# Computer control methods
|
|
89
|
-
def left_click(self, project_id: str, x: int, y: int) -> Dict[str, Any]:
|
|
90
|
-
return self._request("POST", f"computers/{project_id}/click", {
|
|
91
|
-
"button": "left", "x": x, "y": y
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
def right_click(self, project_id: str, x: int, y: int) -> Dict[str, Any]:
|
|
95
|
-
return self._request("POST", f"computers/{project_id}/click", {
|
|
96
|
-
"button": "right", "x": x, "y": y
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
def double_click(self, project_id: str, x: int, y: int) -> Dict[str, Any]:
|
|
100
|
-
return self._request("POST", f"computers/{project_id}/click", {
|
|
101
|
-
"button": "left", "x": x, "y": y, "double": True
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
def drag(self, project_id: str, start_x: int, start_y: int,
|
|
105
|
-
end_x: int, end_y: int, button: str = "left",
|
|
106
|
-
duration: float = 0.5) -> Dict[str, Any]:
|
|
107
|
-
"""Perform a drag operation from start to end coordinates"""
|
|
108
|
-
return self._request("POST", f"computers/{project_id}/drag", {
|
|
109
|
-
"start_x": start_x,
|
|
110
|
-
"start_y": start_y,
|
|
111
|
-
"end_x": end_x,
|
|
112
|
-
"end_y": end_y,
|
|
113
|
-
"button": button,
|
|
114
|
-
"duration": duration
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
def scroll(self, project_id: str, direction: str, amount: int) -> Dict[str, Any]:
|
|
118
|
-
return self._request("POST", f"computers/{project_id}/scroll", {
|
|
119
|
-
"direction": direction, "amount": amount
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
def type_text(self, project_id: str, text: str) -> Dict[str, Any]:
|
|
123
|
-
return self._request("POST", f"computers/{project_id}/type", {
|
|
124
|
-
"text": text
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
def key_press(self, project_id: str, key: str) -> Dict[str, Any]:
|
|
128
|
-
return self._request("POST", f"computers/{project_id}/key", {
|
|
129
|
-
"key": key
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
def get_screenshot(self, project_id: str) -> Dict[str, Any]:
|
|
133
|
-
return self._request("GET", f"computers/{project_id}/screenshot")
|
|
134
|
-
|
|
135
|
-
def execute_bash(self, project_id: str, command: str) -> Dict[str, Any]:
|
|
136
|
-
return self._request("POST", f"computers/{project_id}/bash", {
|
|
137
|
-
"command": command
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
def execute_python(self, project_id: str, code: str, timeout: int = 10) -> Dict[str, Any]:
|
|
141
|
-
"""Execute Python code on the computer"""
|
|
142
|
-
return self._request("POST", f"computers/{project_id}/exec", {
|
|
143
|
-
"code": code,
|
|
144
|
-
"timeout": timeout
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
def wait(self, project_id: str, seconds: float) -> Dict[str, Any]:
|
|
148
|
-
return self._request("POST", f"computers/{project_id}/wait", {
|
|
149
|
-
"seconds": seconds
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
# Streaming methods
|
|
153
|
-
def start_stream(self, project_id: str, connection_name: str) -> Dict[str, Any]:
|
|
154
|
-
"""Start streaming to a configured RTMP connection"""
|
|
155
|
-
return self._request("POST", f"computers/{project_id}/stream/start", {
|
|
156
|
-
"connection_name": connection_name
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
def stop_stream(self, project_id: str) -> Dict[str, Any]:
|
|
160
|
-
"""Stop the active stream"""
|
|
161
|
-
return self._request("POST", f"computers/{project_id}/stream/stop")
|
|
162
|
-
|
|
163
|
-
def get_stream_status(self, project_id: str) -> Dict[str, Any]:
|
|
164
|
-
"""Get current stream status"""
|
|
165
|
-
return self._request("GET", f"computers/{project_id}/stream/status")
|
orgo-0.0.24/src/orgo/computer.py
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
"""Computer class for interacting with Orgo virtual environments"""
|
|
2
|
-
import os
|
|
3
|
-
import io
|
|
4
|
-
import base64
|
|
5
|
-
import logging
|
|
6
|
-
from typing import Dict, List, Any, Optional, Callable, Literal
|
|
7
|
-
from PIL import Image
|
|
8
|
-
import requests
|
|
9
|
-
from requests.exceptions import RequestException
|
|
10
|
-
|
|
11
|
-
from .api.client import ApiClient
|
|
12
|
-
from .prompt import get_provider
|
|
13
|
-
from .project import ProjectManager
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
class Computer:
|
|
18
|
-
def __init__(self,
|
|
19
|
-
project_id: Optional[str] = None,
|
|
20
|
-
api_key: Optional[str] = None,
|
|
21
|
-
config: Optional[Dict[str, Any]] = None,
|
|
22
|
-
base_api_url: Optional[str] = None,
|
|
23
|
-
ram: Optional[Literal[2, 4]] = None,
|
|
24
|
-
memory: Optional[Literal[2, 4]] = None,
|
|
25
|
-
cpu: Optional[Literal[2, 4]] = None):
|
|
26
|
-
"""
|
|
27
|
-
Initialize an Orgo virtual computer.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
project_id: Existing project ID to connect to (optional)
|
|
31
|
-
api_key: Orgo API key (defaults to ORGO_API_KEY env var)
|
|
32
|
-
config: Configuration for new computer (optional)
|
|
33
|
-
base_api_url: Custom API URL (optional)
|
|
34
|
-
ram: RAM in GB for new computer (2 or 4) - only used when creating
|
|
35
|
-
memory: Alternative parameter for RAM in GB (2 or 4) - only used when creating
|
|
36
|
-
cpu: CPU cores for new computer (2 or 4) - only used when creating
|
|
37
|
-
|
|
38
|
-
Note: If both ram and memory are provided, ram takes precedence.
|
|
39
|
-
"""
|
|
40
|
-
self.api_key = api_key or os.environ.get("ORGO_API_KEY")
|
|
41
|
-
self.base_api_url = base_api_url
|
|
42
|
-
self.api = ApiClient(self.api_key, self.base_api_url)
|
|
43
|
-
|
|
44
|
-
# Handle memory parameter as an alias for ram
|
|
45
|
-
if ram is None and memory is not None:
|
|
46
|
-
ram = memory
|
|
47
|
-
|
|
48
|
-
# Look for a saved project ID if none was provided
|
|
49
|
-
if project_id is None:
|
|
50
|
-
project_id = ProjectManager.load_project_id()
|
|
51
|
-
|
|
52
|
-
if project_id:
|
|
53
|
-
try:
|
|
54
|
-
self.project_id = project_id
|
|
55
|
-
self._info = self.api.connect_computer(project_id)
|
|
56
|
-
# Log if ram/memory/cpu were provided but ignored
|
|
57
|
-
if ram is not None or memory is not None or cpu is not None:
|
|
58
|
-
logger.info("Note: ram, memory, and cpu parameters are ignored when connecting to existing computer")
|
|
59
|
-
except (RequestException, ValueError) as e:
|
|
60
|
-
logger.warning(f"Could not connect to saved project {project_id}: {e}")
|
|
61
|
-
self._create_new_computer(config, ram, cpu)
|
|
62
|
-
else:
|
|
63
|
-
self._create_new_computer(config, ram, cpu)
|
|
64
|
-
|
|
65
|
-
def _create_new_computer(self, config: Optional[Dict[str, Any]] = None,
|
|
66
|
-
ram: Optional[Literal[2, 4]] = None,
|
|
67
|
-
cpu: Optional[Literal[2, 4]] = None):
|
|
68
|
-
"""Create a new computer instance and save its ID"""
|
|
69
|
-
# Validate ram and cpu values if provided
|
|
70
|
-
if ram is not None and ram not in [2, 4]:
|
|
71
|
-
raise ValueError("ram/memory must be either 2 or 4 GB")
|
|
72
|
-
if cpu is not None and cpu not in [2, 4]:
|
|
73
|
-
raise ValueError("cpu must be either 2 or 4 cores")
|
|
74
|
-
|
|
75
|
-
# Build the config with ram and cpu if provided
|
|
76
|
-
if ram is not None or cpu is not None:
|
|
77
|
-
if config is None:
|
|
78
|
-
config = {}
|
|
79
|
-
else:
|
|
80
|
-
# Make a copy to avoid modifying the original
|
|
81
|
-
config = config.copy()
|
|
82
|
-
|
|
83
|
-
# Add ram and cpu to config
|
|
84
|
-
if ram is not None:
|
|
85
|
-
config['ram'] = ram
|
|
86
|
-
if cpu is not None:
|
|
87
|
-
config['cpu'] = cpu
|
|
88
|
-
|
|
89
|
-
response = self.api.create_computer(config)
|
|
90
|
-
self.project_id = response.get("name")
|
|
91
|
-
self._info = response
|
|
92
|
-
|
|
93
|
-
if not self.project_id:
|
|
94
|
-
raise ValueError("Failed to initialize computer: No project ID returned")
|
|
95
|
-
|
|
96
|
-
# Save the project ID for future use
|
|
97
|
-
ProjectManager.save_project_id(self.project_id)
|
|
98
|
-
|
|
99
|
-
def status(self) -> Dict[str, Any]:
|
|
100
|
-
"""Get current computer status"""
|
|
101
|
-
return self.api.get_status(self.project_id)
|
|
102
|
-
|
|
103
|
-
def start(self) -> Dict[str, Any]:
|
|
104
|
-
"""Start the computer"""
|
|
105
|
-
return self.api.start_computer(self.project_id)
|
|
106
|
-
|
|
107
|
-
def stop(self) -> Dict[str, Any]:
|
|
108
|
-
"""Stop the computer"""
|
|
109
|
-
return self.api.stop_computer(self.project_id)
|
|
110
|
-
|
|
111
|
-
def restart(self) -> Dict[str, Any]:
|
|
112
|
-
"""Restart the computer"""
|
|
113
|
-
return self.api.restart_computer(self.project_id)
|
|
114
|
-
|
|
115
|
-
def destroy(self) -> Dict[str, Any]:
|
|
116
|
-
"""Terminate and delete the computer instance"""
|
|
117
|
-
result = self.api.delete_computer(self.project_id)
|
|
118
|
-
# Clear the local project cache after destroying
|
|
119
|
-
ProjectManager.clear_project_cache()
|
|
120
|
-
return result
|
|
121
|
-
|
|
122
|
-
# Navigation methods
|
|
123
|
-
def left_click(self, x: int, y: int) -> Dict[str, Any]:
|
|
124
|
-
"""Perform left mouse click at specified coordinates"""
|
|
125
|
-
return self.api.left_click(self.project_id, x, y)
|
|
126
|
-
|
|
127
|
-
def right_click(self, x: int, y: int) -> Dict[str, Any]:
|
|
128
|
-
"""Perform right mouse click at specified coordinates"""
|
|
129
|
-
return self.api.right_click(self.project_id, x, y)
|
|
130
|
-
|
|
131
|
-
def double_click(self, x: int, y: int) -> Dict[str, Any]:
|
|
132
|
-
"""Perform double click at specified coordinates"""
|
|
133
|
-
return self.api.double_click(self.project_id, x, y)
|
|
134
|
-
|
|
135
|
-
def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
|
136
|
-
button: str = "left", duration: float = 0.5) -> Dict[str, Any]:
|
|
137
|
-
"""
|
|
138
|
-
Perform a smooth drag operation from start to end coordinates.
|
|
139
|
-
|
|
140
|
-
Args:
|
|
141
|
-
start_x: Starting X coordinate
|
|
142
|
-
start_y: Starting Y coordinate
|
|
143
|
-
end_x: Ending X coordinate
|
|
144
|
-
end_y: Ending Y coordinate
|
|
145
|
-
button: Mouse button to use ("left" or "right", default: "left")
|
|
146
|
-
duration: Duration of the drag in seconds (0.1 to 5.0, default: 0.5)
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
Dict with operation result
|
|
150
|
-
"""
|
|
151
|
-
return self.api.drag(self.project_id, start_x, start_y, end_x, end_y, button, duration)
|
|
152
|
-
|
|
153
|
-
def scroll(self, direction: str = "down", amount: int = 1) -> Dict[str, Any]:
|
|
154
|
-
"""Scroll in specified direction and amount"""
|
|
155
|
-
return self.api.scroll(self.project_id, direction, amount)
|
|
156
|
-
|
|
157
|
-
# Input methods
|
|
158
|
-
def type(self, text: str) -> Dict[str, Any]:
|
|
159
|
-
"""Type the specified text"""
|
|
160
|
-
return self.api.type_text(self.project_id, text)
|
|
161
|
-
|
|
162
|
-
def key(self, key: str) -> Dict[str, Any]:
|
|
163
|
-
"""Press a key or key combination (e.g., "Enter", "ctrl+c")"""
|
|
164
|
-
return self.api.key_press(self.project_id, key)
|
|
165
|
-
|
|
166
|
-
# View methods
|
|
167
|
-
def screenshot(self) -> Image.Image:
|
|
168
|
-
"""Capture screenshot and return as PIL Image"""
|
|
169
|
-
response = self.api.get_screenshot(self.project_id)
|
|
170
|
-
image_data = response.get("image", "")
|
|
171
|
-
|
|
172
|
-
# Check if it's a URL (new format) or base64 (legacy format)
|
|
173
|
-
if image_data.startswith(('http://', 'https://')):
|
|
174
|
-
# Download image from URL
|
|
175
|
-
img_response = requests.get(image_data)
|
|
176
|
-
img_response.raise_for_status()
|
|
177
|
-
return Image.open(io.BytesIO(img_response.content))
|
|
178
|
-
else:
|
|
179
|
-
# Legacy base64 format
|
|
180
|
-
img_data = base64.b64decode(image_data)
|
|
181
|
-
return Image.open(io.BytesIO(img_data))
|
|
182
|
-
|
|
183
|
-
def screenshot_base64(self) -> str:
|
|
184
|
-
"""Capture screenshot and return as base64 string"""
|
|
185
|
-
response = self.api.get_screenshot(self.project_id)
|
|
186
|
-
image_data = response.get("image", "")
|
|
187
|
-
|
|
188
|
-
# Check if it's a URL (new format) or base64 (legacy format)
|
|
189
|
-
if image_data.startswith(('http://', 'https://')):
|
|
190
|
-
# Download image from URL and convert to base64
|
|
191
|
-
img_response = requests.get(image_data)
|
|
192
|
-
img_response.raise_for_status()
|
|
193
|
-
return base64.b64encode(img_response.content).decode('utf-8')
|
|
194
|
-
else:
|
|
195
|
-
# Already base64
|
|
196
|
-
return image_data
|
|
197
|
-
|
|
198
|
-
# Execution methods
|
|
199
|
-
def bash(self, command: str) -> str:
|
|
200
|
-
"""Execute a bash command and return output"""
|
|
201
|
-
response = self.api.execute_bash(self.project_id, command)
|
|
202
|
-
return response.get("output", "")
|
|
203
|
-
|
|
204
|
-
def exec(self, code: str, timeout: int = 10) -> Dict[str, Any]:
|
|
205
|
-
"""
|
|
206
|
-
Execute Python code on the remote computer.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
code: Python code to execute
|
|
210
|
-
timeout: Maximum execution time in seconds (default: 10, max: 300)
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
Dict with keys:
|
|
214
|
-
- success: bool indicating if execution completed without errors
|
|
215
|
-
- output: str containing stdout output
|
|
216
|
-
- error: str containing error message if any
|
|
217
|
-
- error_type: str with exception type name if error occurred
|
|
218
|
-
- timeout: bool indicating if execution timed out
|
|
219
|
-
|
|
220
|
-
Example:
|
|
221
|
-
result = computer.exec('''
|
|
222
|
-
import os
|
|
223
|
-
print(f"Current directory: {os.getcwd()}")
|
|
224
|
-
print(f"Files: {os.listdir('.')}")
|
|
225
|
-
''')
|
|
226
|
-
|
|
227
|
-
if result['success']:
|
|
228
|
-
print(result['output'])
|
|
229
|
-
else:
|
|
230
|
-
print(f"Error: {result['error']}")
|
|
231
|
-
"""
|
|
232
|
-
response = self.api.execute_python(self.project_id, code, timeout)
|
|
233
|
-
return response
|
|
234
|
-
|
|
235
|
-
def wait(self, seconds: float) -> Dict[str, Any]:
|
|
236
|
-
"""Wait for specified number of seconds"""
|
|
237
|
-
return self.api.wait(self.project_id, seconds)
|
|
238
|
-
|
|
239
|
-
# Streaming methods
|
|
240
|
-
def start_stream(self, connection: str) -> Dict[str, Any]:
|
|
241
|
-
"""
|
|
242
|
-
Start streaming the computer screen to an RTMP server.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
connection: Name of the RTMP connection configured in settings (e.g., "my-twitch-1")
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
Dict with streaming status information
|
|
249
|
-
|
|
250
|
-
Example:
|
|
251
|
-
# First configure a connection in settings at https://www.orgo.ai/settings
|
|
252
|
-
# Then start streaming
|
|
253
|
-
computer.start_stream("my-twitch-1")
|
|
254
|
-
|
|
255
|
-
# Do your demo/automation
|
|
256
|
-
computer.type("Hello viewers!")
|
|
257
|
-
|
|
258
|
-
# Stop streaming when done
|
|
259
|
-
computer.stop_stream()
|
|
260
|
-
"""
|
|
261
|
-
return self.api.start_stream(self.project_id, connection)
|
|
262
|
-
|
|
263
|
-
def stop_stream(self) -> Dict[str, Any]:
|
|
264
|
-
"""
|
|
265
|
-
Stop the active stream.
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
Dict with stop status information
|
|
269
|
-
"""
|
|
270
|
-
return self.api.stop_stream(self.project_id)
|
|
271
|
-
|
|
272
|
-
def stream_status(self) -> Dict[str, Any]:
|
|
273
|
-
"""
|
|
274
|
-
Get the current streaming status.
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
Dict with keys:
|
|
278
|
-
- status: "idle", "streaming", or "terminated"
|
|
279
|
-
- start_time: ISO timestamp when stream started (if streaming)
|
|
280
|
-
- pid: Process ID of ffmpeg (if streaming)
|
|
281
|
-
"""
|
|
282
|
-
return self.api.get_stream_status(self.project_id)
|
|
283
|
-
|
|
284
|
-
# AI control method
|
|
285
|
-
def prompt(self,
|
|
286
|
-
instruction: str,
|
|
287
|
-
provider: str = "anthropic",
|
|
288
|
-
model: str = "claude-3-7-sonnet-20250219",
|
|
289
|
-
display_width: int = 1024,
|
|
290
|
-
display_height: int = 768,
|
|
291
|
-
callback: Optional[Callable[[str, Any], None]] = None,
|
|
292
|
-
thinking_enabled: bool = False,
|
|
293
|
-
thinking_budget: int = 1024,
|
|
294
|
-
max_tokens: int = 4096,
|
|
295
|
-
max_iterations: int = 20,
|
|
296
|
-
max_saved_screenshots: int = 5,
|
|
297
|
-
api_key: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
298
|
-
"""
|
|
299
|
-
Control the computer with natural language instructions using an AI assistant.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
instruction: What you want the AI to do with the computer
|
|
303
|
-
provider: AI provider to use (default: "anthropic")
|
|
304
|
-
model: Model to use (default: "claude-3-7-sonnet-20250219")
|
|
305
|
-
display_width: Screen width in pixels
|
|
306
|
-
display_height: Screen height in pixels
|
|
307
|
-
callback: Optional callback function for progress updates
|
|
308
|
-
thinking_enabled: Enable Claude's thinking capability (default: False)
|
|
309
|
-
thinking_budget: Token budget for thinking (default: 1024)
|
|
310
|
-
max_tokens: Maximum tokens for model response
|
|
311
|
-
max_iterations: Maximum number of agent loop iterations
|
|
312
|
-
max_saved_screenshots: Maximum number of screenshots to keep in history (default: 5)
|
|
313
|
-
api_key: API key for the AI provider (defaults to env var)
|
|
314
|
-
|
|
315
|
-
Returns:
|
|
316
|
-
List of messages from the conversation
|
|
317
|
-
"""
|
|
318
|
-
# Get the provider instance
|
|
319
|
-
provider_instance = get_provider(provider)
|
|
320
|
-
|
|
321
|
-
# Execute the prompt
|
|
322
|
-
return provider_instance.execute(
|
|
323
|
-
computer_id=self.project_id,
|
|
324
|
-
instruction=instruction,
|
|
325
|
-
callback=callback,
|
|
326
|
-
api_key=api_key,
|
|
327
|
-
model=model,
|
|
328
|
-
display_width=display_width,
|
|
329
|
-
display_height=display_height,
|
|
330
|
-
thinking_enabled=thinking_enabled,
|
|
331
|
-
thinking_budget=thinking_budget,
|
|
332
|
-
max_tokens=max_tokens,
|
|
333
|
-
max_iterations=max_iterations,
|
|
334
|
-
max_saved_screenshots=max_saved_screenshots,
|
|
335
|
-
# Pass through the Orgo API client configuration
|
|
336
|
-
orgo_api_key=self.api_key,
|
|
337
|
-
orgo_base_url=self.base_api_url
|
|
338
|
-
)
|
orgo-0.0.24/src/orgo/project.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# src/orgo/project.py
|
|
2
|
-
"""Project management for Orgo virtual environments"""
|
|
3
|
-
import os
|
|
4
|
-
import json
|
|
5
|
-
import shutil
|
|
6
|
-
import logging
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
class ProjectManager:
|
|
12
|
-
"""Manages project persistence for Orgo computers"""
|
|
13
|
-
|
|
14
|
-
@staticmethod
|
|
15
|
-
def load_project_id() -> Optional[str]:
|
|
16
|
-
"""Load project ID from local config file"""
|
|
17
|
-
config_path = ProjectManager._get_config_path()
|
|
18
|
-
|
|
19
|
-
if not os.path.exists(config_path):
|
|
20
|
-
return None
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
with open(config_path, 'r') as f:
|
|
24
|
-
data = json.load(f)
|
|
25
|
-
return data.get('project_id')
|
|
26
|
-
except (json.JSONDecodeError, IOError, OSError) as e:
|
|
27
|
-
logger.warning(f"Error loading project config: {str(e)}")
|
|
28
|
-
return None
|
|
29
|
-
|
|
30
|
-
@staticmethod
|
|
31
|
-
def save_project_id(project_id: str) -> None:
|
|
32
|
-
"""Save project ID to local config file"""
|
|
33
|
-
config_dir = ProjectManager._get_project_dir()
|
|
34
|
-
config_path = ProjectManager._get_config_path()
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
os.makedirs(config_dir, exist_ok=True)
|
|
38
|
-
with open(config_path, 'w') as f:
|
|
39
|
-
json.dump({'project_id': project_id}, f, indent=2)
|
|
40
|
-
except (IOError, OSError) as e:
|
|
41
|
-
logger.error(f"Failed to save project ID: {str(e)}")
|
|
42
|
-
raise RuntimeError(f"Failed to save project configuration: {str(e)}") from e
|
|
43
|
-
|
|
44
|
-
@staticmethod
|
|
45
|
-
def clear_project_cache() -> None:
|
|
46
|
-
"""Clear the .orgo folder and all its contents"""
|
|
47
|
-
project_dir = ProjectManager._get_project_dir()
|
|
48
|
-
|
|
49
|
-
if os.path.exists(project_dir):
|
|
50
|
-
try:
|
|
51
|
-
shutil.rmtree(project_dir)
|
|
52
|
-
logger.info(f"Cleared project cache at {project_dir}")
|
|
53
|
-
except (IOError, OSError) as e:
|
|
54
|
-
logger.warning(f"Failed to clear project cache: {str(e)}")
|
|
55
|
-
|
|
56
|
-
@staticmethod
|
|
57
|
-
def _get_project_dir() -> str:
|
|
58
|
-
"""Get the project directory path"""
|
|
59
|
-
return os.path.join(os.getcwd(), ".orgo")
|
|
60
|
-
|
|
61
|
-
@staticmethod
|
|
62
|
-
def _get_config_path() -> str:
|
|
63
|
-
"""Get the full path to the config file"""
|
|
64
|
-
return os.path.join(ProjectManager._get_project_dir(), "project.json")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|