orgo 0.0.35__py3-none-any.whl → 0.0.37__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/computer.py CHANGED
@@ -1,4 +1,13 @@
1
- """Computer class for interacting with Orgo virtual environments"""
1
+ """
2
+ Orgo Computer - Control virtual computers with AI.
3
+
4
+ Usage:
5
+ from orgo import Computer
6
+
7
+ computer = Computer(computer_id="your-computer-id")
8
+ computer.prompt("Open Firefox and search for AI news")
9
+ """
10
+
2
11
  import os as operating_system
3
12
  import base64
4
13
  import logging
@@ -14,7 +23,25 @@ from .prompt import get_provider
14
23
 
15
24
  logger = logging.getLogger(__name__)
16
25
 
26
+
17
27
  class Computer:
28
+ """
29
+ Control an Orgo virtual computer.
30
+
31
+ Examples:
32
+ # Connect to existing computer
33
+ computer = Computer(computer_id="abc123")
34
+
35
+ # Create new computer in project
36
+ computer = Computer(project="my-project", ram=4, cpu=2)
37
+
38
+ # AI control (uses Orgo by default)
39
+ computer.prompt("Open Firefox")
40
+
41
+ # AI control with Anthropic directly
42
+ computer.prompt("Open Firefox", provider="anthropic")
43
+ """
44
+
18
45
  def __init__(self,
19
46
  project: Optional[Union[str, 'Project']] = None,
20
47
  name: Optional[str] = None,
@@ -31,187 +58,136 @@ class Computer:
31
58
  Initialize an Orgo virtual computer.
32
59
 
33
60
  Args:
34
- project: Project name (str) or Project instance. If not provided, creates a new project.
35
- name: Computer name within the project (optional, auto-generated if not provided)
36
- computer_id: Existing computer ID to connect to (optional)
61
+ computer_id: Connect to existing computer by ID
62
+ project: Project name or Project instance
63
+ name: Computer name (auto-generated if not provided)
37
64
  api_key: Orgo API key (defaults to ORGO_API_KEY env var)
38
- base_api_url: Custom API URL (optional)
39
- ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, or 64) - only used when creating
40
- cpu: CPU cores (1, 2, 4, 8, or 16) - only used when creating
41
- os: Operating system ("linux" or "windows") - only used when creating
42
- gpu: GPU type - only used when creating
43
- image: Custom image reference (str) or Forge object - only used when creating
44
-
45
- Examples:
46
- # Create computer in new project
47
- computer = Computer(ram=4, cpu=2)
48
-
49
- # Create computer with custom image
50
- forge = Forge(org_id="myorg", project_id="myproj").base("ubuntu").run("echo hello")
51
- computer = Computer(image=forge)
52
-
53
- # Create computer in existing project
54
- computer = Computer(project="manus", ram=4, cpu=2)
55
-
56
- # Connect to existing computer by ID
57
- computer = Computer(computer_id="11c4fd46-e069-4c32-be65-f82d9f87b9b8")
65
+ base_api_url: Custom API URL
66
+ ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, 64)
67
+ cpu: CPU cores (1, 2, 4, 8, 16)
68
+ os: "linux" or "windows"
69
+ gpu: "none", "a10", "l40s", "a100-40gb", "a100-80gb"
70
+ image: Custom image reference or Forge object
58
71
  """
59
72
  self.api_key = api_key or operating_system.environ.get("ORGO_API_KEY")
60
73
  self.base_api_url = base_api_url
61
74
  self.api = ApiClient(self.api_key, self.base_api_url)
62
75
 
63
- # Handle memory parameter as an alias for ram
64
76
  if ram is None and memory is not None:
65
77
  ram = memory
66
78
 
67
- # Store configuration
68
79
  self.os = os or "linux"
69
80
  self.ram = ram or 2
70
81
  self.cpu = cpu or 2
71
82
  self.gpu = gpu or "none"
72
-
73
- # Handle image
74
83
  self.image = image
84
+
75
85
  if hasattr(self.image, 'build') and callable(self.image.build):
76
86
  logger.info("Building image from Forge object...")
77
87
  self.image = self.image.build()
78
88
 
79
89
  if computer_id:
80
-
81
- # Just store the computer ID, no API call needed
82
90
  self.computer_id = computer_id
83
91
  self.name = name
84
92
  self.project_id = None
85
93
  self.project_name = None
86
- logger.info(f"Connected to computer ID: {self.computer_id}")
94
+ logger.info(f"Connected to computer: {self.computer_id}")
87
95
  elif project:
88
- # Work with specified project
89
96
  if isinstance(project, str):
90
- # Project name provided
91
97
  self.project_name = project
92
98
  self._initialize_with_project_name(project, name)
93
99
  else:
94
- # Project instance provided
95
100
  from .project import Project as ProjectClass
96
101
  if isinstance(project, ProjectClass):
97
102
  self.project_name = project.name
98
103
  self.project_id = project.id
99
104
  self._initialize_with_project_instance(project, name)
100
105
  else:
101
- raise ValueError("project must be a string (project name) or Project instance")
106
+ raise ValueError("project must be a string or Project instance")
102
107
  else:
103
- # No project specified, create a new one
104
108
  self._create_new_project_and_computer(name)
105
109
 
110
+ # =========================================================================
111
+ # Initialization Helpers
112
+ # =========================================================================
113
+
106
114
  def _initialize_with_project_name(self, project_name: str, computer_name: Optional[str]):
107
- """Initialize with a project name (create project if needed)"""
108
115
  try:
109
- # Try to get existing project
110
116
  project = self.api.get_project_by_name(project_name)
111
117
  self.project_id = project.get("id")
112
-
113
- # Check for existing computers
114
118
  computers = self.api.list_computers(self.project_id)
115
119
 
116
120
  if computer_name:
117
- # Look for specific computer
118
121
  existing = next((c for c in computers if c.get("name") == computer_name), None)
119
122
  if existing:
120
123
  self._connect_to_existing_computer(existing)
121
124
  else:
122
- # Create new computer with specified name
123
125
  self._create_computer(self.project_id, computer_name)
124
126
  elif computers:
125
- # No name specified, use first available computer
126
127
  self._connect_to_existing_computer(computers[0])
127
128
  else:
128
- # No computers exist, create new one
129
129
  self._create_computer(self.project_id, computer_name)
130
-
131
130
  except Exception:
132
- # Project doesn't exist, create it
133
- logger.info(f"Project {project_name} not found, creating new project")
131
+ logger.info(f"Creating new project: {project_name}")
134
132
  project = self.api.create_project(project_name)
135
133
  self.project_id = project.get("id")
136
134
  self._create_computer(self.project_id, computer_name)
137
135
 
138
136
  def _initialize_with_project_instance(self, project: 'Project', computer_name: Optional[str]):
139
- """Initialize with a Project instance"""
140
137
  computers = project.list_computers()
141
138
 
142
139
  if computer_name:
143
- # Look for specific computer
144
140
  existing = next((c for c in computers if c.get("name") == computer_name), None)
145
141
  if existing:
146
142
  self._connect_to_existing_computer(existing)
147
143
  else:
148
- # Create new computer with specified name
149
144
  self._create_computer(project.id, computer_name)
150
145
  elif computers:
151
- # No name specified, use first available computer
152
146
  self._connect_to_existing_computer(computers[0])
153
147
  else:
154
- # No computers exist, create new one
155
148
  self._create_computer(project.id, computer_name)
156
149
 
157
150
  def _create_new_project_and_computer(self, computer_name: Optional[str]):
158
- """Create a new project and computer"""
159
- # Generate a unique project name
160
151
  project_name = f"project-{uuid.uuid4().hex[:8]}"
161
-
162
- # Create the project
163
152
  project = self.api.create_project(project_name)
164
153
  self.project_id = project.get("id")
165
154
  self.project_name = project_name
166
-
167
- # Create a computer in the new project
168
155
  self._create_computer(self.project_id, computer_name)
169
156
 
170
157
  def _connect_to_existing_computer(self, computer_info: Dict[str, Any]):
171
- """Connect to an existing computer"""
172
158
  self.computer_id = computer_info.get("id")
173
159
  self.name = computer_info.get("name")
174
- logger.info(f"Connected to existing computer {self.name} (ID: {self.computer_id})")
160
+ logger.info(f"Connected to: {self.name} ({self.computer_id})")
175
161
 
176
162
  def _create_computer(self, project_id: str, computer_name: Optional[str]):
177
- """Create a new computer in the project"""
178
- # Generate name if not provided
179
163
  if not computer_name:
180
164
  computer_name = f"desktop-{uuid.uuid4().hex[:8]}"
181
165
 
182
166
  self.name = computer_name
183
167
 
184
- # Validate parameters
168
+ # Validate
185
169
  if self.ram not in [1, 2, 4, 8, 16, 32, 64]:
186
- raise ValueError("ram must be one of: 1, 2, 4, 8, 16, 32, 64 GB")
170
+ raise ValueError("ram must be: 1, 2, 4, 8, 16, 32, or 64 GB")
187
171
  if self.cpu not in [1, 2, 4, 8, 16]:
188
- raise ValueError("cpu must be one of: 1, 2, 4, 8, 16 cores")
172
+ raise ValueError("cpu must be: 1, 2, 4, 8, or 16 cores")
189
173
  if self.os not in ["linux", "windows"]:
190
- raise ValueError("os must be either 'linux' or 'windows'")
174
+ raise ValueError("os must be: 'linux' or 'windows'")
191
175
  if self.gpu not in ["none", "a10", "l40s", "a100-40gb", "a100-80gb"]:
192
- raise ValueError("gpu must be one of: 'none', 'a10', 'l40s', 'a100-40gb', 'a100-80gb'")
193
-
194
- # Resolve image name if needed
176
+ raise ValueError("gpu must be: 'none', 'a10', 'l40s', 'a100-40gb', or 'a100-80gb'")
177
+
178
+ # Resolve image
195
179
  image_ref = self.image
196
180
  if image_ref and isinstance(image_ref, str) and not image_ref.startswith("registry.fly.io"):
197
- logger.info(f"Resolving image name '{image_ref}'...")
198
181
  try:
199
- # Try to get org_id from project info
200
182
  project_info = self.api.get_project(project_id)
201
- org_id = project_info.get("org_id", "orgo") # Default to 'orgo'
202
-
183
+ org_id = project_info.get("org_id", "orgo")
203
184
  response = self.api.get_latest_build(org_id, project_id, image_ref)
204
185
  if response and response.get("build"):
205
- resolved_ref = response.get("build", {}).get("imageRef")
206
- if resolved_ref:
207
- logger.info(f"Resolved '{image_ref}' to '{resolved_ref}'")
208
- image_ref = resolved_ref
209
- else:
210
- logger.warning(f"Build found for '{image_ref}' but no imageRef present.")
211
- else:
212
- logger.warning(f"Could not resolve image name '{self.image}'. Using as is.")
186
+ resolved = response.get("build", {}).get("imageRef")
187
+ if resolved:
188
+ image_ref = resolved
213
189
  except Exception as e:
214
- logger.warning(f"Failed to resolve image name: {e}")
190
+ logger.warning(f"Failed to resolve image: {e}")
215
191
 
216
192
  computer = self.api.create_computer(
217
193
  project_id=project_id,
@@ -223,54 +199,67 @@ class Computer:
223
199
  image=image_ref
224
200
  )
225
201
  self.computer_id = computer.get("id")
226
- logger.info(f"Created new computer {self.name} (ID: {self.computer_id})")
202
+ logger.info(f"Created: {self.name} ({self.computer_id})")
203
+
204
+ # =========================================================================
205
+ # Computer Management
206
+ # =========================================================================
227
207
 
228
208
  def status(self) -> Dict[str, Any]:
229
- """Get current computer status"""
209
+ """Get current computer status."""
230
210
  return self.api.get_computer(self.computer_id)
231
211
 
232
212
  def restart(self) -> Dict[str, Any]:
233
- """Restart the computer"""
213
+ """Restart the computer."""
234
214
  return self.api.restart_computer(self.computer_id)
235
215
 
236
216
  def destroy(self) -> Dict[str, Any]:
237
- """Terminate and delete the computer instance"""
217
+ """Delete the computer."""
238
218
  return self.api.delete_computer(self.computer_id)
239
219
 
240
- # Navigation methods
220
+ # =========================================================================
221
+ # Mouse Actions
222
+ # =========================================================================
223
+
241
224
  def left_click(self, x: int, y: int) -> Dict[str, Any]:
242
- """Perform left mouse click at specified coordinates"""
225
+ """Left click at coordinates."""
243
226
  return self.api.left_click(self.computer_id, x, y)
244
227
 
245
228
  def right_click(self, x: int, y: int) -> Dict[str, Any]:
246
- """Perform right mouse click at specified coordinates"""
229
+ """Right click at coordinates."""
247
230
  return self.api.right_click(self.computer_id, x, y)
248
231
 
249
232
  def double_click(self, x: int, y: int) -> Dict[str, Any]:
250
- """Perform double click at specified coordinates"""
233
+ """Double click at coordinates."""
251
234
  return self.api.double_click(self.computer_id, x, y)
252
235
 
253
236
  def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
254
237
  button: str = "left", duration: float = 0.5) -> Dict[str, Any]:
255
- """Perform a smooth drag operation from start to end coordinates"""
238
+ """Drag from start to end coordinates."""
256
239
  return self.api.drag(self.computer_id, start_x, start_y, end_x, end_y, button, duration)
257
240
 
258
241
  def scroll(self, direction: str = "down", amount: int = 3) -> Dict[str, Any]:
259
- """Scroll in specified direction and amount"""
242
+ """Scroll in direction."""
260
243
  return self.api.scroll(self.computer_id, direction, amount)
261
244
 
262
- # Input methods
245
+ # =========================================================================
246
+ # Keyboard Actions
247
+ # =========================================================================
248
+
263
249
  def type(self, text: str) -> Dict[str, Any]:
264
- """Type the specified text"""
250
+ """Type text."""
265
251
  return self.api.type_text(self.computer_id, text)
266
252
 
267
253
  def key(self, key: str) -> Dict[str, Any]:
268
- """Press a key or key combination (e.g., "Enter", "ctrl+c")"""
254
+ """Press key (e.g., "Enter", "ctrl+c")."""
269
255
  return self.api.key_press(self.computer_id, key)
270
256
 
271
- # View methods
257
+ # =========================================================================
258
+ # Screen Capture
259
+ # =========================================================================
260
+
272
261
  def screenshot(self) -> Image.Image:
273
- """Capture screenshot and return as PIL Image"""
262
+ """Capture screenshot as PIL Image."""
274
263
  response = self.api.get_screenshot(self.computer_id)
275
264
  image_data = response.get("image", "")
276
265
 
@@ -279,11 +268,10 @@ class Computer:
279
268
  img_response.raise_for_status()
280
269
  return Image.open(io.BytesIO(img_response.content))
281
270
  else:
282
- img_data = base64.b64decode(image_data)
283
- return Image.open(io.BytesIO(img_data))
271
+ return Image.open(io.BytesIO(base64.b64decode(image_data)))
284
272
 
285
273
  def screenshot_base64(self) -> str:
286
- """Capture screenshot and return as base64 string"""
274
+ """Capture screenshot as base64 string."""
287
275
  response = self.api.get_screenshot(self.computer_id)
288
276
  image_data = response.get("image", "")
289
277
 
@@ -291,58 +279,102 @@ class Computer:
291
279
  img_response = requests.get(image_data)
292
280
  img_response.raise_for_status()
293
281
  return base64.b64encode(img_response.content).decode('utf-8')
294
- else:
295
- return image_data
282
+ return image_data
283
+
284
+ # =========================================================================
285
+ # Code Execution
286
+ # =========================================================================
296
287
 
297
- # Execution methods
298
288
  def bash(self, command: str) -> str:
299
- """Execute a bash command and return output"""
289
+ """Execute bash command."""
300
290
  response = self.api.execute_bash(self.computer_id, command)
301
291
  return response.get("output", "")
302
292
 
303
293
  def exec(self, code: str, timeout: int = 10) -> Dict[str, Any]:
304
- """Execute Python code on the remote computer"""
305
- response = self.api.execute_python(self.computer_id, code, timeout)
306
- return response
294
+ """Execute Python code."""
295
+ return self.api.execute_python(self.computer_id, code, timeout)
307
296
 
308
297
  def wait(self, seconds: float) -> Dict[str, Any]:
309
- """Wait for specified number of seconds"""
298
+ """Wait for seconds."""
310
299
  return self.api.wait(self.computer_id, seconds)
311
300
 
312
- # Streaming methods
301
+ # =========================================================================
302
+ # Streaming
303
+ # =========================================================================
304
+
313
305
  def start_stream(self, connection: str) -> Dict[str, Any]:
314
- """Start streaming the computer screen to an RTMP server"""
306
+ """Start RTMP stream."""
315
307
  return self.api.start_stream(self.computer_id, connection)
316
308
 
317
309
  def stop_stream(self) -> Dict[str, Any]:
318
- """Stop the active stream"""
310
+ """Stop stream."""
319
311
  return self.api.stop_stream(self.computer_id)
320
312
 
321
313
  def stream_status(self) -> Dict[str, Any]:
322
- """Get the current streaming status"""
314
+ """Get stream status."""
323
315
  return self.api.get_stream_status(self.computer_id)
324
316
 
325
- # AI control method
317
+ # =========================================================================
318
+ # AI Control
319
+ # =========================================================================
320
+
326
321
  def prompt(self,
327
322
  instruction: str,
328
- provider: str = "anthropic",
323
+ provider: Optional[str] = None,
324
+ verbose: bool = True,
325
+ callback: Optional[Callable[[str, Any], None]] = None,
329
326
  model: str = "claude-sonnet-4-5-20250929",
330
327
  display_width: int = 1024,
331
328
  display_height: int = 768,
332
- callback: Optional[Callable[[str, Any], None]] = None,
333
329
  thinking_enabled: bool = True,
334
330
  thinking_budget: int = 1024,
335
331
  max_tokens: int = 4096,
336
332
  max_iterations: int = 100,
337
333
  max_saved_screenshots: int = 3,
334
+ system_prompt: Optional[str] = None,
338
335
  api_key: Optional[str] = None) -> List[Dict[str, Any]]:
339
- """Control the computer with natural language instructions using an AI assistant"""
336
+ """
337
+ Control the computer with natural language.
338
+
339
+ Args:
340
+ instruction: What you want the computer to do
341
+ provider: "orgo" (default) or "anthropic"
342
+ verbose: Show progress logs (default: True)
343
+ callback: Optional callback for events
344
+ model: AI model to use
345
+ display_width: Screen width
346
+ display_height: Screen height
347
+ thinking_enabled: Enable extended thinking
348
+ thinking_budget: Token budget for thinking
349
+ max_tokens: Max response tokens
350
+ max_iterations: Max agent iterations
351
+ max_saved_screenshots: Screenshots to keep in context
352
+ system_prompt: Custom instructions
353
+ api_key: Anthropic key (only for provider="anthropic")
354
+
355
+ Returns:
356
+ List of conversation messages
357
+
358
+ Examples:
359
+ # Default: Uses Orgo hosted agent
360
+ computer.prompt("Open Firefox and search for AI news")
361
+
362
+ # Quiet mode (no logs)
363
+ computer.prompt("Open Firefox", verbose=False)
364
+
365
+ # Use Anthropic directly
366
+ computer.prompt("Open Firefox", provider="anthropic")
367
+
368
+ # With callback
369
+ computer.prompt("Search Google", callback=lambda t, d: print(f"{t}: {d}"))
370
+ """
340
371
  provider_instance = get_provider(provider)
341
372
 
342
373
  return provider_instance.execute(
343
374
  computer_id=self.computer_id,
344
375
  instruction=instruction,
345
376
  callback=callback,
377
+ verbose=verbose,
346
378
  api_key=api_key,
347
379
  model=model,
348
380
  display_width=display_width,
@@ -352,11 +384,21 @@ class Computer:
352
384
  max_tokens=max_tokens,
353
385
  max_iterations=max_iterations,
354
386
  max_saved_screenshots=max_saved_screenshots,
387
+ system_prompt=system_prompt,
355
388
  orgo_api_key=self.api_key,
356
389
  orgo_base_url=self.base_api_url
357
390
  )
358
391
 
392
+ # =========================================================================
393
+ # URL Helper
394
+ # =========================================================================
395
+
396
+ @property
397
+ def url(self) -> str:
398
+ """Get the URL to view this computer."""
399
+ return f"https://orgo.ai/workspaces/{self.computer_id}"
400
+
359
401
  def __repr__(self):
360
- project_str = f", project='{self.project_name}'" if hasattr(self, 'project_name') and self.project_name else ""
361
- name_str = f"name='{self.name}'" if hasattr(self, 'name') and self.name else f"id='{self.computer_id}'"
362
- return f"Computer({name_str}{project_str})"
402
+ if hasattr(self, 'name') and self.name:
403
+ return f"Computer(name='{self.name}', id='{self.computer_id}')"
404
+ return f"Computer(id='{self.computer_id}')"