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