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