orgo 0.0.35__tar.gz → 0.0.38__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orgo
3
- Version: 0.0.35
3
+ Version: 0.0.38
4
4
  Summary: Computers for AI agents
5
5
  Author: Orgo Team
6
6
  License: MIT
@@ -8,8 +8,10 @@ Project-URL: Homepage, https://www.orgo.ai
8
8
  Project-URL: Documentation, https://docs.orgo.ai
9
9
  Requires-Python: >=3.7
10
10
  Description-Content-Type: text/markdown
11
- Requires-Dist: requests>=2.25.0
12
- Requires-Dist: pillow>=8.0.0
11
+ Requires-Dist: requests>=2.28.0
12
+ Requires-Dist: pillow>=9.0.0
13
+ Requires-Dist: anthropic>=0.50.0
14
+ Requires-Dist: websocket-client>=1.6.0
13
15
 
14
16
  # Orgo SDK
15
17
 
@@ -4,15 +4,17 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "orgo"
7
- version = "0.0.35"
7
+ version = "0.0.38"
8
8
  description = "Computers for AI agents"
9
9
  authors = [{name = "Orgo Team"}]
10
10
  license = {text = "MIT"}
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.7"
13
13
  dependencies = [
14
- "requests>=2.25.0",
15
- "pillow>=8.0.0",
14
+ "requests>=2.28.0",
15
+ "pillow>=9.0.0",
16
+ "anthropic>=0.50.0",
17
+ "websocket-client>=1.6.0",
16
18
  ]
17
19
 
18
20
  [project.urls]
@@ -0,0 +1,467 @@
1
+ """
2
+ Orgo Computer - Control virtual computers with AI.
3
+
4
+ Usage:
5
+ from orgo import Computer
6
+
7
+ computer = Computer(project="your-project")
8
+ computer.prompt("Open Firefox and search for AI news")
9
+ """
10
+
11
+ import os as operating_system
12
+ import base64
13
+ import logging
14
+ import uuid
15
+ import io
16
+ import random
17
+ from typing import Dict, List, Any, Optional, Callable, Literal, Union
18
+ from PIL import Image
19
+ import requests
20
+ from requests.exceptions import RequestException
21
+
22
+ from .api.client import ApiClient
23
+ from .prompt import get_provider
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _generate_computer_name() -> str:
29
+ """Generate a random computer name like 'computer-1568'"""
30
+ return f"computer-{random.randint(1000, 9999)}"
31
+
32
+
33
+ def _print_success(message: str):
34
+ """Print a success message with nice formatting"""
35
+ print(f"✓ {message}")
36
+
37
+
38
+ def _print_error(message: str):
39
+ """Print an error message with nice formatting"""
40
+ print(f"✗ {message}")
41
+
42
+
43
+ def _print_info(message: str):
44
+ """Print an info message with nice formatting"""
45
+ print(f"→ {message}")
46
+
47
+
48
+ class Computer:
49
+ """
50
+ Control an Orgo virtual computer.
51
+
52
+ Examples:
53
+ # Create computer in new/existing project
54
+ computer = Computer(project="my-project")
55
+
56
+ # Create with specific name
57
+ computer = Computer(project="my-project", name="dev-machine")
58
+
59
+ # Connect to existing computer by ID
60
+ computer = Computer(computer_id="abc123")
61
+
62
+ # AI control (uses Orgo by default)
63
+ computer.prompt("Open Firefox")
64
+
65
+ # AI control with Anthropic directly
66
+ computer.prompt("Open Firefox", provider="anthropic")
67
+ """
68
+
69
+ def __init__(self,
70
+ project: Optional[Union[str, 'Project']] = None,
71
+ name: Optional[str] = None,
72
+ computer_id: Optional[str] = None,
73
+ api_key: Optional[str] = None,
74
+ base_api_url: Optional[str] = None,
75
+ ram: Optional[Literal[1, 2, 4, 8, 16, 32, 64]] = None,
76
+ memory: Optional[Literal[1, 2, 4, 8, 16, 32, 64]] = None,
77
+ cpu: Optional[Literal[1, 2, 4, 8, 16]] = None,
78
+ os: Optional[Literal["linux", "windows"]] = None,
79
+ gpu: Optional[Literal["none", "a10", "l40s", "a100-40gb", "a100-80gb"]] = None,
80
+ image: Optional[Union[str, Any]] = None,
81
+ verbose: bool = True):
82
+ """
83
+ Initialize an Orgo virtual computer.
84
+
85
+ Args:
86
+ project: Project name or Project instance (creates if doesn't exist)
87
+ name: Computer name (auto-generated if not provided)
88
+ computer_id: Connect to existing computer by ID
89
+ api_key: Orgo API key (defaults to ORGO_API_KEY env var)
90
+ base_api_url: Custom API URL
91
+ ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, 64)
92
+ cpu: CPU cores (1, 2, 4, 8, 16)
93
+ os: "linux" or "windows"
94
+ gpu: "none", "a10", "l40s", "a100-40gb", "a100-80gb"
95
+ image: Custom image reference or Forge object
96
+ verbose: Show console output (default: True)
97
+ """
98
+ self.api_key = api_key or operating_system.environ.get("ORGO_API_KEY")
99
+ self.base_api_url = base_api_url
100
+ self.api = ApiClient(self.api_key, self.base_api_url)
101
+ self.verbose = verbose
102
+
103
+ if ram is None and memory is not None:
104
+ ram = memory
105
+
106
+ self.os = os or "linux"
107
+ self.ram = ram or 2
108
+ self.cpu = cpu or 2
109
+ self.gpu = gpu or "none"
110
+ self.image = image
111
+
112
+ if hasattr(self.image, 'build') and callable(self.image.build):
113
+ if self.verbose:
114
+ _print_info("Building image from Forge object...")
115
+ self.image = self.image.build()
116
+
117
+ if computer_id:
118
+ self.computer_id = computer_id
119
+ self.name = name
120
+ self.project_id = None
121
+ self.project_name = None
122
+ if self.verbose:
123
+ _print_success(f"Connected to computer: {self.computer_id}")
124
+ elif project:
125
+ if isinstance(project, str):
126
+ self.project_name = project
127
+ self._initialize_with_project_name(project, name)
128
+ else:
129
+ from .project import Project as ProjectClass
130
+ if isinstance(project, ProjectClass):
131
+ self.project_name = project.name
132
+ self.project_id = project.id
133
+ self._initialize_with_project_instance(project, name)
134
+ else:
135
+ raise ValueError("project must be a string or Project instance")
136
+ else:
137
+ self._create_new_project_and_computer(name)
138
+
139
+ # =========================================================================
140
+ # Initialization Helpers
141
+ # =========================================================================
142
+
143
+ def _initialize_with_project_name(self, project_name: str, computer_name: Optional[str]):
144
+ """Initialize computer with project name (create project if needed)"""
145
+ try:
146
+ # Try to get existing project
147
+ project = self.api.get_project_by_name(project_name)
148
+ self.project_id = project.get("id")
149
+
150
+ # If no computer name specified, generate one
151
+ if not computer_name:
152
+ computer_name = _generate_computer_name()
153
+
154
+ # Create the computer in this project
155
+ self._create_computer(self.project_id, computer_name, project_name)
156
+
157
+ except Exception:
158
+ # Project doesn't exist, create it
159
+ if self.verbose:
160
+ _print_info(f"Creating project: {project_name}")
161
+ project = self.api.create_project(project_name)
162
+ self.project_id = project.get("id")
163
+
164
+ # Generate name if not specified
165
+ if not computer_name:
166
+ computer_name = _generate_computer_name()
167
+
168
+ self._create_computer(self.project_id, computer_name, project_name)
169
+
170
+ def _initialize_with_project_instance(self, project: 'Project', computer_name: Optional[str]):
171
+ """Initialize computer with Project instance"""
172
+ # Generate name if not specified
173
+ if not computer_name:
174
+ computer_name = _generate_computer_name()
175
+
176
+ self._create_computer(project.id, computer_name, project.name)
177
+
178
+ def _create_new_project_and_computer(self, computer_name: Optional[str]):
179
+ """Create a new project and computer when no project specified"""
180
+ project_name = f"project-{uuid.uuid4().hex[:8]}"
181
+
182
+ if self.verbose:
183
+ _print_info(f"Creating project: {project_name}")
184
+
185
+ project = self.api.create_project(project_name)
186
+ self.project_id = project.get("id")
187
+ self.project_name = project_name
188
+
189
+ # Generate name if not specified
190
+ if not computer_name:
191
+ computer_name = _generate_computer_name()
192
+
193
+ self._create_computer(self.project_id, computer_name, project_name)
194
+
195
+ def _connect_to_existing_computer(self, computer_info: Dict[str, Any]):
196
+ """Connect to an existing computer"""
197
+ self.computer_id = computer_info.get("id")
198
+ self.name = computer_info.get("name")
199
+ if self.verbose:
200
+ _print_success(f"Connected to: {self.name} ({self.computer_id})")
201
+
202
+ def _create_computer(self, project_id: str, computer_name: str, project_name: str):
203
+ """Create a new computer with beautiful console output"""
204
+ self.name = computer_name
205
+
206
+ # Validate parameters
207
+ if self.ram not in [1, 2, 4, 8, 16, 32, 64]:
208
+ raise ValueError("ram must be: 1, 2, 4, 8, 16, 32, or 64 GB")
209
+ if self.cpu not in [1, 2, 4, 8, 16]:
210
+ raise ValueError("cpu must be: 1, 2, 4, 8, or 16 cores")
211
+ if self.os not in ["linux", "windows"]:
212
+ raise ValueError("os must be: 'linux' or 'windows'")
213
+ if self.gpu not in ["none", "a10", "l40s", "a100-40gb", "a100-80gb"]:
214
+ raise ValueError("gpu must be: 'none', 'a10', 'l40s', 'a100-40gb', or 'a100-80gb'")
215
+
216
+ # Resolve image if needed
217
+ image_ref = self.image
218
+ if image_ref and isinstance(image_ref, str) and not image_ref.startswith("registry.fly.io"):
219
+ try:
220
+ project_info = self.api.get_project(project_id)
221
+ org_id = project_info.get("org_id", "orgo")
222
+ response = self.api.get_latest_build(org_id, project_id, image_ref)
223
+ if response and response.get("build"):
224
+ resolved = response.get("build", {}).get("imageRef")
225
+ if resolved:
226
+ image_ref = resolved
227
+ except Exception as e:
228
+ if self.verbose:
229
+ logger.warning(f"Failed to resolve image: {e}")
230
+
231
+ # Create the computer
232
+ try:
233
+ computer = self.api.create_computer(
234
+ project_id=project_id,
235
+ computer_name=computer_name,
236
+ os=self.os,
237
+ ram=self.ram,
238
+ cpu=self.cpu,
239
+ gpu=self.gpu,
240
+ image=image_ref
241
+ )
242
+ self.computer_id = computer.get("id")
243
+
244
+ # Beautiful success message
245
+ if self.verbose:
246
+ _print_success(
247
+ f"Computer [{self.name}] successfully created under workspace [{project_name}]"
248
+ )
249
+ _print_info(f"ID: {self.computer_id}")
250
+ _print_info(f"View at: https://orgo.ai/workspaces/{self.computer_id}")
251
+
252
+ except Exception as e:
253
+ if self.verbose:
254
+ _print_error(f"Failed to create computer: {str(e)}")
255
+ raise
256
+
257
+ # =========================================================================
258
+ # Computer Management
259
+ # =========================================================================
260
+
261
+ def status(self) -> Dict[str, Any]:
262
+ """Get current computer status."""
263
+ return self.api.get_computer(self.computer_id)
264
+
265
+ def restart(self) -> Dict[str, Any]:
266
+ """Restart the computer."""
267
+ if self.verbose:
268
+ _print_info(f"Restarting computer: {self.name}")
269
+ result = self.api.restart_computer(self.computer_id)
270
+ if self.verbose:
271
+ _print_success("Computer restarted")
272
+ return result
273
+
274
+ def destroy(self) -> Dict[str, Any]:
275
+ """Delete the computer."""
276
+ if self.verbose:
277
+ _print_info(f"Deleting computer: {self.name}")
278
+ result = self.api.delete_computer(self.computer_id)
279
+ if self.verbose:
280
+ _print_success("Computer deleted")
281
+ return result
282
+
283
+ # =========================================================================
284
+ # Mouse Actions
285
+ # =========================================================================
286
+
287
+ def left_click(self, x: int, y: int) -> Dict[str, Any]:
288
+ """Left click at coordinates."""
289
+ return self.api.left_click(self.computer_id, x, y)
290
+
291
+ def right_click(self, x: int, y: int) -> Dict[str, Any]:
292
+ """Right click at coordinates."""
293
+ return self.api.right_click(self.computer_id, x, y)
294
+
295
+ def double_click(self, x: int, y: int) -> Dict[str, Any]:
296
+ """Double click at coordinates."""
297
+ return self.api.double_click(self.computer_id, x, y)
298
+
299
+ def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
300
+ button: str = "left", duration: float = 0.5) -> Dict[str, Any]:
301
+ """Drag from start to end coordinates."""
302
+ return self.api.drag(self.computer_id, start_x, start_y, end_x, end_y, button, duration)
303
+
304
+ def scroll(self, direction: str = "down", amount: int = 3) -> Dict[str, Any]:
305
+ """Scroll in direction."""
306
+ return self.api.scroll(self.computer_id, direction, amount)
307
+
308
+ # =========================================================================
309
+ # Keyboard Actions
310
+ # =========================================================================
311
+
312
+ def type(self, text: str) -> Dict[str, Any]:
313
+ """Type text."""
314
+ return self.api.type_text(self.computer_id, text)
315
+
316
+ def key(self, key: str) -> Dict[str, Any]:
317
+ """Press key (e.g., "Enter", "ctrl+c")."""
318
+ return self.api.key_press(self.computer_id, key)
319
+
320
+ # =========================================================================
321
+ # Screen Capture
322
+ # =========================================================================
323
+
324
+ def screenshot(self) -> Image.Image:
325
+ """Capture screenshot as PIL Image."""
326
+ response = self.api.get_screenshot(self.computer_id)
327
+ image_data = response.get("image", "")
328
+
329
+ if image_data.startswith(('http://', 'https://')):
330
+ img_response = requests.get(image_data)
331
+ img_response.raise_for_status()
332
+ return Image.open(io.BytesIO(img_response.content))
333
+ else:
334
+ return Image.open(io.BytesIO(base64.b64decode(image_data)))
335
+
336
+ def screenshot_base64(self) -> str:
337
+ """Capture screenshot as base64 string."""
338
+ response = self.api.get_screenshot(self.computer_id)
339
+ image_data = response.get("image", "")
340
+
341
+ if image_data.startswith(('http://', 'https://')):
342
+ img_response = requests.get(image_data)
343
+ img_response.raise_for_status()
344
+ return base64.b64encode(img_response.content).decode('utf-8')
345
+ return image_data
346
+
347
+ # =========================================================================
348
+ # Code Execution
349
+ # =========================================================================
350
+
351
+ def bash(self, command: str) -> str:
352
+ """Execute bash command."""
353
+ response = self.api.execute_bash(self.computer_id, command)
354
+ return response.get("output", "")
355
+
356
+ def exec(self, code: str, timeout: int = 10) -> Dict[str, Any]:
357
+ """Execute Python code."""
358
+ return self.api.execute_python(self.computer_id, code, timeout)
359
+
360
+ def wait(self, seconds: float) -> Dict[str, Any]:
361
+ """Wait for seconds."""
362
+ return self.api.wait(self.computer_id, seconds)
363
+
364
+ # =========================================================================
365
+ # Streaming
366
+ # =========================================================================
367
+
368
+ def start_stream(self, connection: str) -> Dict[str, Any]:
369
+ """Start RTMP stream."""
370
+ return self.api.start_stream(self.computer_id, connection)
371
+
372
+ def stop_stream(self) -> Dict[str, Any]:
373
+ """Stop stream."""
374
+ return self.api.stop_stream(self.computer_id)
375
+
376
+ def stream_status(self) -> Dict[str, Any]:
377
+ """Get stream status."""
378
+ return self.api.get_stream_status(self.computer_id)
379
+
380
+ # =========================================================================
381
+ # AI Control
382
+ # =========================================================================
383
+
384
+ def prompt(self,
385
+ instruction: str,
386
+ provider: Optional[str] = None,
387
+ verbose: bool = True,
388
+ callback: Optional[Callable[[str, Any], None]] = None,
389
+ model: str = "claude-sonnet-4-5-20250929",
390
+ display_width: int = 1024,
391
+ display_height: int = 768,
392
+ thinking_enabled: bool = True,
393
+ thinking_budget: int = 1024,
394
+ max_tokens: int = 4096,
395
+ max_iterations: int = 100,
396
+ max_saved_screenshots: int = 3,
397
+ system_prompt: Optional[str] = None,
398
+ api_key: Optional[str] = None) -> List[Dict[str, Any]]:
399
+ """
400
+ Control the computer with natural language.
401
+
402
+ Args:
403
+ instruction: What you want the computer to do
404
+ provider: "orgo" (default) or "anthropic"
405
+ verbose: Show progress logs (default: True)
406
+ callback: Optional callback for events
407
+ model: AI model to use
408
+ display_width: Screen width
409
+ display_height: Screen height
410
+ thinking_enabled: Enable extended thinking
411
+ thinking_budget: Token budget for thinking
412
+ max_tokens: Max response tokens
413
+ max_iterations: Max agent iterations
414
+ max_saved_screenshots: Screenshots to keep in context
415
+ system_prompt: Custom instructions
416
+ api_key: Anthropic key (only for provider="anthropic")
417
+
418
+ Returns:
419
+ List of conversation messages
420
+
421
+ Examples:
422
+ # Default: Uses Orgo hosted agent
423
+ computer.prompt("Open Firefox and search for AI news")
424
+
425
+ # Quiet mode (no logs)
426
+ computer.prompt("Open Firefox", verbose=False)
427
+
428
+ # Use Anthropic directly
429
+ computer.prompt("Open Firefox", provider="anthropic")
430
+
431
+ # With callback
432
+ computer.prompt("Search Google", callback=lambda t, d: print(f"{t}: {d}"))
433
+ """
434
+ provider_instance = get_provider(provider)
435
+
436
+ return provider_instance.execute(
437
+ computer_id=self.computer_id,
438
+ instruction=instruction,
439
+ callback=callback,
440
+ verbose=verbose,
441
+ api_key=api_key,
442
+ model=model,
443
+ display_width=display_width,
444
+ display_height=display_height,
445
+ thinking_enabled=thinking_enabled,
446
+ thinking_budget=thinking_budget,
447
+ max_tokens=max_tokens,
448
+ max_iterations=max_iterations,
449
+ max_saved_screenshots=max_saved_screenshots,
450
+ system_prompt=system_prompt,
451
+ orgo_api_key=self.api_key,
452
+ orgo_base_url=self.base_api_url
453
+ )
454
+
455
+ # =========================================================================
456
+ # URL Helper
457
+ # =========================================================================
458
+
459
+ @property
460
+ def url(self) -> str:
461
+ """Get the URL to view this computer."""
462
+ return f"https://orgo.ai/workspaces/{self.computer_id}"
463
+
464
+ def __repr__(self):
465
+ if hasattr(self, 'name') and self.name:
466
+ return f"Computer(name='{self.name}', id='{self.computer_id}')"
467
+ return f"Computer(id='{self.computer_id}')"