orgo 0.0.24__py3-none-any.whl → 0.0.26__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
@@ -3,39 +3,52 @@ import os
3
3
  import io
4
4
  import base64
5
5
  import logging
6
- from typing import Dict, List, Any, Optional, Callable, Literal
6
+ import uuid
7
+ from typing import Dict, List, Any, Optional, Callable, Literal, Union
7
8
  from PIL import Image
8
9
  import requests
9
10
  from requests.exceptions import RequestException
10
11
 
11
12
  from .api.client import ApiClient
12
13
  from .prompt import get_provider
13
- from .project import ProjectManager
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
  class Computer:
18
18
  def __init__(self,
19
- project_id: Optional[str] = None,
19
+ project: Optional[Union[str, 'Project']] = None,
20
+ name: Optional[str] = None,
21
+ computer_id: Optional[str] = None,
20
22
  api_key: Optional[str] = None,
21
- config: Optional[Dict[str, Any]] = None,
22
23
  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):
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):
26
29
  """
27
30
  Initialize an Orgo virtual computer.
28
31
 
29
32
  Args:
30
- project_id: Existing project ID to connect to (optional)
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)
31
36
  api_key: Orgo API key (defaults to ORGO_API_KEY env var)
32
- config: Configuration for new computer (optional)
33
37
  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
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
37
42
 
38
- Note: If both ram and memory are provided, ram takes precedence.
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")
39
52
  """
40
53
  self.api_key = api_key or os.environ.get("ORGO_API_KEY")
41
54
  self.base_api_url = base_api_url
@@ -45,241 +58,246 @@ class Computer:
45
58
  if ram is None and memory is not None:
46
59
  ram = memory
47
60
 
48
- # Look for a saved project ID if none was provided
49
- if project_id is None:
50
- project_id = ProjectManager.load_project_id()
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"
51
66
 
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)
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")
62
85
  else:
63
- self._create_new_computer(config, ram, cpu)
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)
64
105
 
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")
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()
74
131
 
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 = {}
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)
79
137
  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
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]}"
88
151
 
89
- response = self.api.create_computer(config)
90
- self.project_id = response.get("name")
91
- self._info = response
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
92
156
 
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)
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})")
98
196
 
99
197
  def status(self) -> Dict[str, Any]:
100
198
  """Get current computer status"""
101
- return self.api.get_status(self.project_id)
199
+ return self.api.get_computer(self.computer_id)
102
200
 
103
201
  def start(self) -> Dict[str, Any]:
104
202
  """Start the computer"""
105
- return self.api.start_computer(self.project_id)
203
+ return self.api.start_computer(self.computer_id)
106
204
 
107
205
  def stop(self) -> Dict[str, Any]:
108
206
  """Stop the computer"""
109
- return self.api.stop_computer(self.project_id)
207
+ return self.api.stop_computer(self.computer_id)
110
208
 
111
209
  def restart(self) -> Dict[str, Any]:
112
210
  """Restart the computer"""
113
- return self.api.restart_computer(self.project_id)
211
+ return self.api.restart_computer(self.computer_id)
114
212
 
115
213
  def destroy(self) -> Dict[str, Any]:
116
214
  """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
215
+ return self.api.delete_computer(self.computer_id)
121
216
 
122
217
  # Navigation methods
123
218
  def left_click(self, x: int, y: int) -> Dict[str, Any]:
124
219
  """Perform left mouse click at specified coordinates"""
125
- return self.api.left_click(self.project_id, x, y)
220
+ return self.api.left_click(self.computer_id, x, y)
126
221
 
127
222
  def right_click(self, x: int, y: int) -> Dict[str, Any]:
128
223
  """Perform right mouse click at specified coordinates"""
129
- return self.api.right_click(self.project_id, x, y)
224
+ return self.api.right_click(self.computer_id, x, y)
130
225
 
131
226
  def double_click(self, x: int, y: int) -> Dict[str, Any]:
132
227
  """Perform double click at specified coordinates"""
133
- return self.api.double_click(self.project_id, x, y)
228
+ return self.api.double_click(self.computer_id, x, y)
134
229
 
135
230
  def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
136
231
  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)
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)
152
234
 
153
- def scroll(self, direction: str = "down", amount: int = 1) -> Dict[str, Any]:
235
+ def scroll(self, direction: str = "down", amount: int = 3) -> Dict[str, Any]:
154
236
  """Scroll in specified direction and amount"""
155
- return self.api.scroll(self.project_id, direction, amount)
237
+ return self.api.scroll(self.computer_id, direction, amount)
156
238
 
157
239
  # Input methods
158
240
  def type(self, text: str) -> Dict[str, Any]:
159
241
  """Type the specified text"""
160
- return self.api.type_text(self.project_id, text)
242
+ return self.api.type_text(self.computer_id, text)
161
243
 
162
244
  def key(self, key: str) -> Dict[str, Any]:
163
245
  """Press a key or key combination (e.g., "Enter", "ctrl+c")"""
164
- return self.api.key_press(self.project_id, key)
246
+ return self.api.key_press(self.computer_id, key)
165
247
 
166
248
  # View methods
167
249
  def screenshot(self) -> Image.Image:
168
250
  """Capture screenshot and return as PIL Image"""
169
- response = self.api.get_screenshot(self.project_id)
251
+ response = self.api.get_screenshot(self.computer_id)
170
252
  image_data = response.get("image", "")
171
253
 
172
- # Check if it's a URL (new format) or base64 (legacy format)
173
254
  if image_data.startswith(('http://', 'https://')):
174
- # Download image from URL
175
255
  img_response = requests.get(image_data)
176
256
  img_response.raise_for_status()
177
257
  return Image.open(io.BytesIO(img_response.content))
178
258
  else:
179
- # Legacy base64 format
180
259
  img_data = base64.b64decode(image_data)
181
260
  return Image.open(io.BytesIO(img_data))
182
261
 
183
262
  def screenshot_base64(self) -> str:
184
263
  """Capture screenshot and return as base64 string"""
185
- response = self.api.get_screenshot(self.project_id)
264
+ response = self.api.get_screenshot(self.computer_id)
186
265
  image_data = response.get("image", "")
187
266
 
188
- # Check if it's a URL (new format) or base64 (legacy format)
189
267
  if image_data.startswith(('http://', 'https://')):
190
- # Download image from URL and convert to base64
191
268
  img_response = requests.get(image_data)
192
269
  img_response.raise_for_status()
193
270
  return base64.b64encode(img_response.content).decode('utf-8')
194
271
  else:
195
- # Already base64
196
272
  return image_data
197
273
 
198
274
  # Execution methods
199
275
  def bash(self, command: str) -> str:
200
276
  """Execute a bash command and return output"""
201
- response = self.api.execute_bash(self.project_id, command)
277
+ response = self.api.execute_bash(self.computer_id, command)
202
278
  return response.get("output", "")
203
279
 
204
280
  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)
281
+ """Execute Python code on the remote computer"""
282
+ response = self.api.execute_python(self.computer_id, code, timeout)
233
283
  return response
234
284
 
235
285
  def wait(self, seconds: float) -> Dict[str, Any]:
236
286
  """Wait for specified number of seconds"""
237
- return self.api.wait(self.project_id, seconds)
287
+ return self.api.wait(self.computer_id, seconds)
238
288
 
239
289
  # Streaming methods
240
290
  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)
291
+ """Start streaming the computer screen to an RTMP server"""
292
+ return self.api.start_stream(self.computer_id, connection)
262
293
 
263
294
  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)
295
+ """Stop the active stream"""
296
+ return self.api.stop_stream(self.computer_id)
271
297
 
272
298
  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)
299
+ """Get the current streaming status"""
300
+ return self.api.get_stream_status(self.computer_id)
283
301
 
284
302
  # AI control method
285
303
  def prompt(self,
@@ -295,32 +313,11 @@ print(f"Files: {os.listdir('.')}")
295
313
  max_iterations: int = 20,
296
314
  max_saved_screenshots: int = 5,
297
315
  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
316
+ """Control the computer with natural language instructions using an AI assistant"""
319
317
  provider_instance = get_provider(provider)
320
318
 
321
- # Execute the prompt
322
319
  return provider_instance.execute(
323
- computer_id=self.project_id,
320
+ computer_id=self.computer_id,
324
321
  instruction=instruction,
325
322
  callback=callback,
326
323
  api_key=api_key,
@@ -332,7 +329,9 @@ print(f"Files: {os.listdir('.')}")
332
329
  max_tokens=max_tokens,
333
330
  max_iterations=max_iterations,
334
331
  max_saved_screenshots=max_saved_screenshots,
335
- # Pass through the Orgo API client configuration
336
332
  orgo_api_key=self.api_key,
337
333
  orgo_base_url=self.base_api_url
338
- )
334
+ )
335
+
336
+ def __repr__(self):
337
+ return f"Computer(name='{self.name}', project='{self.project_name}', id='{self.computer_id}')"
orgo/project.py CHANGED
@@ -1,64 +1,87 @@
1
- # src/orgo/project.py
2
- """Project management for Orgo virtual environments"""
1
+ """Project class for managing Orgo projects"""
3
2
  import os
4
- import json
5
- import shutil
6
- import logging
7
- from typing import Optional
3
+ import uuid
4
+ from typing import Dict, List, Any, Optional
8
5
 
9
- logger = logging.getLogger(__name__)
6
+ from .api.client import ApiClient
10
7
 
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()
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.
18
15
 
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()
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]}"
35
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
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
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
43
46
 
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)}")
47
+ def status(self) -> Dict[str, Any]:
48
+ """Get project status"""
49
+ return self.api.get_project(self.id)
55
50
 
56
- @staticmethod
57
- def _get_project_dir() -> str:
58
- """Get the project directory path"""
59
- return os.path.join(os.getcwd(), ".orgo")
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]
60
85
 
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")
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.24
3
+ Version: 0.0.26
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=w_Cvjw1pspkV14CodgNJpDvYAOvL3tS6KfDiUsEcC3s,14463
3
+ orgo/project.py,sha256=u6kxoUUj9xAxzVWl2A4kD4EHmQWrrwTixkIVRhH4Ehc,3072
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.26.dist-info/METADATA,sha256=o92pMm2FgkRbLYQzgw_LLLRgpiJvyDBjdT7M-ug1m9w,822
10
+ orgo-0.0.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ orgo-0.0.26.dist-info/top_level.txt,sha256=q0rYtFji8GbYuhFW8A5Ab9e0j27761IKPhnL0E9xow4,5
12
+ orgo-0.0.26.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- orgo/__init__.py,sha256=TlOzDJqRKotAam631MdZcz8ypAqyWrLo-Px8HWLkrD0,131
2
- orgo/computer.py,sha256=44jqFqANITalNt-7hsDn-UKVJZCyqR-63wrxAyTnpmw,13629
3
- orgo/project.py,sha256=F90mYJKRZAoFSFw8HVAiosMoLyzVlf0QaKJgTyXoJ4A,2267
4
- orgo/prompt.py,sha256=ynblwXPTDp_aF1MbGBsY0PIEr9naklDaKFcfSE_EZ6E,19781
5
- orgo/api/__init__.py,sha256=9Tzb_OPJ5DH7Cg7OrHzpZZUT4ip05alpa9RLDYmnId8,113
6
- orgo/api/client.py,sha256=MgOg5shKzsOVsablvtTa39_ms-OdrmFMW1pD07C6PTs,7045
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.24.dist-info/METADATA,sha256=uidJBILPzxo19T-6DJS6xYb8JSqSMfxuEU8113xAQMA,822
10
- orgo-0.0.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- orgo-0.0.24.dist-info/top_level.txt,sha256=q0rYtFji8GbYuhFW8A5Ab9e0j27761IKPhnL0E9xow4,5
12
- orgo-0.0.24.dist-info/RECORD,,
File without changes