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