orgo 0.0.37__tar.gz → 0.0.39__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.37
3
+ Version: 0.0.39
4
4
  Summary: Computers for AI agents
5
5
  Author: Orgo Team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "orgo"
7
- version = "0.0.37"
7
+ version = "0.0.39"
8
8
  description = "Computers for AI agents"
9
9
  authors = [{name = "Orgo Team"}]
10
10
  license = {text = "MIT"}
@@ -4,7 +4,7 @@ Orgo Computer - Control virtual computers with AI.
4
4
  Usage:
5
5
  from orgo import Computer
6
6
 
7
- computer = Computer(computer_id="your-computer-id")
7
+ computer = Computer(project="your-project")
8
8
  computer.prompt("Open Firefox and search for AI news")
9
9
  """
10
10
 
@@ -13,6 +13,7 @@ import base64
13
13
  import logging
14
14
  import uuid
15
15
  import io
16
+ import random
16
17
  from typing import Dict, List, Any, Optional, Callable, Literal, Union
17
18
  from PIL import Image
18
19
  import requests
@@ -24,16 +25,39 @@ from .prompt import get_provider
24
25
  logger = logging.getLogger(__name__)
25
26
 
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
+
27
48
  class Computer:
28
49
  """
29
50
  Control an Orgo virtual computer.
30
51
 
31
52
  Examples:
32
- # Connect to existing computer
33
- computer = Computer(computer_id="abc123")
53
+ # Create computer in new/existing project
54
+ computer = Computer(project="my-project")
34
55
 
35
- # Create new computer in project
36
- computer = Computer(project="my-project", ram=4, cpu=2)
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")
37
61
 
38
62
  # AI control (uses Orgo by default)
39
63
  computer.prompt("Open Firefox")
@@ -53,14 +77,15 @@ class Computer:
53
77
  cpu: Optional[Literal[1, 2, 4, 8, 16]] = None,
54
78
  os: Optional[Literal["linux", "windows"]] = None,
55
79
  gpu: Optional[Literal["none", "a10", "l40s", "a100-40gb", "a100-80gb"]] = None,
56
- image: Optional[Union[str, Any]] = None):
80
+ image: Optional[Union[str, Any]] = None,
81
+ verbose: bool = True):
57
82
  """
58
83
  Initialize an Orgo virtual computer.
59
84
 
60
85
  Args:
61
- computer_id: Connect to existing computer by ID
62
- project: Project name or Project instance
86
+ project: Project name or Project instance (creates if doesn't exist)
63
87
  name: Computer name (auto-generated if not provided)
88
+ computer_id: Connect to existing computer by ID
64
89
  api_key: Orgo API key (defaults to ORGO_API_KEY env var)
65
90
  base_api_url: Custom API URL
66
91
  ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, 64)
@@ -68,10 +93,12 @@ class Computer:
68
93
  os: "linux" or "windows"
69
94
  gpu: "none", "a10", "l40s", "a100-40gb", "a100-80gb"
70
95
  image: Custom image reference or Forge object
96
+ verbose: Show console output (default: True)
71
97
  """
72
98
  self.api_key = api_key or operating_system.environ.get("ORGO_API_KEY")
73
99
  self.base_api_url = base_api_url
74
100
  self.api = ApiClient(self.api_key, self.base_api_url)
101
+ self.verbose = verbose
75
102
 
76
103
  if ram is None and memory is not None:
77
104
  ram = memory
@@ -83,7 +110,8 @@ class Computer:
83
110
  self.image = image
84
111
 
85
112
  if hasattr(self.image, 'build') and callable(self.image.build):
86
- logger.info("Building image from Forge object...")
113
+ if self.verbose:
114
+ _print_info("Building image from Forge object...")
87
115
  self.image = self.image.build()
88
116
 
89
117
  if computer_id:
@@ -91,7 +119,8 @@ class Computer:
91
119
  self.name = name
92
120
  self.project_id = None
93
121
  self.project_name = None
94
- logger.info(f"Connected to computer: {self.computer_id}")
122
+ if self.verbose:
123
+ _print_success(f"Connected to computer: {self.computer_id}")
95
124
  elif project:
96
125
  if isinstance(project, str):
97
126
  self.project_name = project
@@ -112,60 +141,69 @@ class Computer:
112
141
  # =========================================================================
113
142
 
114
143
  def _initialize_with_project_name(self, project_name: str, computer_name: Optional[str]):
144
+ """Initialize computer with project name (create project if needed)"""
115
145
  try:
146
+ # Try to get existing project
116
147
  project = self.api.get_project_by_name(project_name)
117
148
  self.project_id = project.get("id")
118
- computers = self.api.list_computers(self.project_id)
119
149
 
120
- if computer_name:
121
- existing = next((c for c in computers if c.get("name") == computer_name), None)
122
- if existing:
123
- self._connect_to_existing_computer(existing)
124
- else:
125
- self._create_computer(self.project_id, computer_name)
126
- elif computers:
127
- self._connect_to_existing_computer(computers[0])
128
- else:
129
- self._create_computer(self.project_id, computer_name)
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
+
130
157
  except Exception:
131
- logger.info(f"Creating new project: {project_name}")
158
+ # Project doesn't exist, create it
159
+ if self.verbose:
160
+ _print_info(f"Creating project: {project_name}")
132
161
  project = self.api.create_project(project_name)
133
162
  self.project_id = project.get("id")
134
- self._create_computer(self.project_id, computer_name)
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)
135
169
 
136
170
  def _initialize_with_project_instance(self, project: 'Project', computer_name: Optional[str]):
137
- computers = project.list_computers()
171
+ """Initialize computer with Project instance"""
172
+ # Generate name if not specified
173
+ if not computer_name:
174
+ computer_name = _generate_computer_name()
138
175
 
139
- if computer_name:
140
- existing = next((c for c in computers if c.get("name") == computer_name), None)
141
- if existing:
142
- self._connect_to_existing_computer(existing)
143
- else:
144
- self._create_computer(project.id, computer_name)
145
- elif computers:
146
- self._connect_to_existing_computer(computers[0])
147
- else:
148
- self._create_computer(project.id, computer_name)
176
+ self._create_computer(project.id, computer_name, project.name)
149
177
 
150
178
  def _create_new_project_and_computer(self, computer_name: Optional[str]):
179
+ """Create a new project and computer when no project specified"""
151
180
  project_name = f"project-{uuid.uuid4().hex[:8]}"
181
+
182
+ if self.verbose:
183
+ _print_info(f"Creating project: {project_name}")
184
+
152
185
  project = self.api.create_project(project_name)
153
186
  self.project_id = project.get("id")
154
187
  self.project_name = project_name
155
- self._create_computer(self.project_id, computer_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)
156
194
 
157
195
  def _connect_to_existing_computer(self, computer_info: Dict[str, Any]):
196
+ """Connect to an existing computer"""
158
197
  self.computer_id = computer_info.get("id")
159
198
  self.name = computer_info.get("name")
160
- logger.info(f"Connected to: {self.name} ({self.computer_id})")
199
+ if self.verbose:
200
+ _print_success(f"Connected to: {self.name} ({self.computer_id})")
161
201
 
162
- def _create_computer(self, project_id: str, computer_name: Optional[str]):
163
- if not computer_name:
164
- computer_name = f"desktop-{uuid.uuid4().hex[:8]}"
165
-
202
+ def _create_computer(self, project_id: str, computer_name: str, project_name: str):
203
+ """Create a new computer with beautiful console output"""
166
204
  self.name = computer_name
167
205
 
168
- # Validate
206
+ # Validate parameters
169
207
  if self.ram not in [1, 2, 4, 8, 16, 32, 64]:
170
208
  raise ValueError("ram must be: 1, 2, 4, 8, 16, 32, or 64 GB")
171
209
  if self.cpu not in [1, 2, 4, 8, 16]:
@@ -175,7 +213,7 @@ class Computer:
175
213
  if self.gpu not in ["none", "a10", "l40s", "a100-40gb", "a100-80gb"]:
176
214
  raise ValueError("gpu must be: 'none', 'a10', 'l40s', 'a100-40gb', or 'a100-80gb'")
177
215
 
178
- # Resolve image
216
+ # Resolve image if needed
179
217
  image_ref = self.image
180
218
  if image_ref and isinstance(image_ref, str) and not image_ref.startswith("registry.fly.io"):
181
219
  try:
@@ -187,19 +225,34 @@ class Computer:
187
225
  if resolved:
188
226
  image_ref = resolved
189
227
  except Exception as e:
190
- logger.warning(f"Failed to resolve image: {e}")
228
+ if self.verbose:
229
+ logger.warning(f"Failed to resolve image: {e}")
191
230
 
192
- computer = self.api.create_computer(
193
- project_id=project_id,
194
- computer_name=computer_name,
195
- os=self.os,
196
- ram=self.ram,
197
- cpu=self.cpu,
198
- gpu=self.gpu,
199
- image=image_ref
200
- )
201
- self.computer_id = computer.get("id")
202
- logger.info(f"Created: {self.name} ({self.computer_id})")
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
203
256
 
204
257
  # =========================================================================
205
258
  # Computer Management
@@ -211,11 +264,21 @@ class Computer:
211
264
 
212
265
  def restart(self) -> Dict[str, Any]:
213
266
  """Restart the computer."""
214
- return self.api.restart_computer(self.computer_id)
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
215
273
 
216
274
  def destroy(self) -> Dict[str, Any]:
217
275
  """Delete the computer."""
218
- return self.api.delete_computer(self.computer_id)
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
219
282
 
220
283
  # =========================================================================
221
284
  # Mouse Actions
@@ -122,6 +122,13 @@ class Console:
122
122
  timestamp = self._c(Colors.DIM, datetime.now().strftime("%H:%M:%S"))
123
123
  print(f" {timestamp} {self._c(Colors.RED, '✗')} {self._c(Colors.RED, message)}")
124
124
 
125
+ def retry(self, attempt: int, max_attempts: int, delay: float):
126
+ """Print retry message."""
127
+ if not self.verbose:
128
+ return
129
+ timestamp = self._c(Colors.DIM, datetime.now().strftime("%H:%M:%S"))
130
+ print(f" {timestamp} {self._c(Colors.YELLOW, '↻')} Retry {attempt}/{max_attempts} in {delay:.1f}s")
131
+
125
132
  def success(self, iterations: int = 0):
126
133
  """Print success message."""
127
134
  if not self.verbose:
@@ -138,6 +145,20 @@ class Console:
138
145
  print()
139
146
 
140
147
 
148
+ # =============================================================================
149
+ # Exceptions
150
+ # =============================================================================
151
+
152
+ class ScreenshotError(Exception):
153
+ """Raised when screenshot capture fails."""
154
+ pass
155
+
156
+
157
+ class TransientVisionError(Exception):
158
+ """Raised when Claude's vision API temporarily fails."""
159
+ pass
160
+
161
+
141
162
  # =============================================================================
142
163
  # System Prompt
143
164
  # =============================================================================
@@ -535,6 +556,8 @@ class AnthropicProvider:
535
556
  thinking_enabled = kwargs.get("thinking_enabled", True)
536
557
  thinking_budget = kwargs.get("thinking_budget", 1024)
537
558
  max_saved_screenshots = kwargs.get("max_saved_screenshots", 3)
559
+ screenshot_retry_attempts = kwargs.get("screenshot_retry_attempts", 3)
560
+ screenshot_retry_delay = kwargs.get("screenshot_retry_delay", 2.0)
538
561
 
539
562
  # System prompt
540
563
  full_system_prompt = get_system_prompt(display_width, display_height, system_prompt)
@@ -581,15 +604,15 @@ class AnthropicProvider:
581
604
  "budget_tokens": thinking_budget
582
605
  }
583
606
 
584
- # Call Claude
585
- try:
586
- response = client.beta.messages.create(**request_params)
587
- except Exception as e:
588
- if "base64" in str(e).lower():
589
- self._prune_screenshots(messages, 1)
590
- response = client.beta.messages.create(**request_params)
591
- else:
592
- raise
607
+ # Call Claude with retry logic
608
+ response = self._call_claude_with_retry(
609
+ client=client,
610
+ request_params=request_params,
611
+ messages=messages,
612
+ console=console,
613
+ max_retries=screenshot_retry_attempts,
614
+ retry_delay=screenshot_retry_delay
615
+ )
593
616
 
594
617
  response_content = response.content
595
618
  messages.append({"role": "assistant", "content": response_content})
@@ -627,11 +650,20 @@ class AnthropicProvider:
627
650
  if callback:
628
651
  callback("tool_use", {"action": action, "params": block.input})
629
652
 
630
- # Execute tools
653
+ # Execute tools with retry logic
631
654
  tool_results = []
632
655
  for block in response_content:
633
656
  if block.type == "tool_use":
634
- result = self._execute_tool(computer_id, block.input, orgo_key, orgo_url, callback)
657
+ result = self._execute_tool_with_retry(
658
+ computer_id=computer_id,
659
+ params=block.input,
660
+ orgo_key=orgo_key,
661
+ orgo_url=orgo_url,
662
+ console=console,
663
+ callback=callback,
664
+ max_retries=screenshot_retry_attempts,
665
+ retry_delay=screenshot_retry_delay
666
+ )
635
667
 
636
668
  tool_result = {"type": "tool_result", "tool_use_id": block.id}
637
669
 
@@ -653,6 +685,128 @@ class AnthropicProvider:
653
685
  console.success(iteration)
654
686
  return messages
655
687
 
688
+ def _call_claude_with_retry(
689
+ self,
690
+ client: anthropic.Anthropic,
691
+ request_params: Dict[str, Any],
692
+ messages: List[Dict[str, Any]],
693
+ console: Console,
694
+ max_retries: int = 3,
695
+ retry_delay: float = 2.0
696
+ ) -> Any:
697
+ """Call Claude API with exponential backoff retry logic."""
698
+
699
+ last_error = None
700
+
701
+ for attempt in range(max_retries):
702
+ try:
703
+ return client.beta.messages.create(**request_params)
704
+
705
+ except anthropic.BadRequestError as e:
706
+ error_msg = str(e).lower()
707
+
708
+ # Check for vision/image processing errors
709
+ if "image" in error_msg or "vision" in error_msg or "could not process" in error_msg:
710
+ last_error = TransientVisionError(f"Vision API error: {e}")
711
+
712
+ if attempt < max_retries - 1:
713
+ delay = retry_delay * (2 ** attempt) # Exponential backoff: 2s, 4s, 8s
714
+ console.retry(attempt + 1, max_retries, delay)
715
+ time.sleep(delay)
716
+
717
+ # Prune screenshots to reduce payload size
718
+ self._prune_screenshots(messages, 1)
719
+ request_params["messages"] = messages
720
+ continue
721
+ else:
722
+ raise last_error
723
+
724
+ # Check for base64 errors (fallback from old code)
725
+ elif "base64" in error_msg:
726
+ if attempt < max_retries - 1:
727
+ delay = retry_delay * (2 ** attempt)
728
+ console.retry(attempt + 1, max_retries, delay)
729
+ time.sleep(delay)
730
+
731
+ self._prune_screenshots(messages, 1)
732
+ request_params["messages"] = messages
733
+ continue
734
+ else:
735
+ raise
736
+ else:
737
+ # Non-retryable error
738
+ raise
739
+
740
+ except (anthropic.APIConnectionError, anthropic.APITimeoutError) as e:
741
+ # Network errors - retry with backoff
742
+ last_error = e
743
+
744
+ if attempt < max_retries - 1:
745
+ delay = retry_delay * (2 ** attempt)
746
+ console.retry(attempt + 1, max_retries, delay)
747
+ time.sleep(delay)
748
+ continue
749
+ else:
750
+ raise
751
+
752
+ except Exception as e:
753
+ # Unexpected errors - don't retry
754
+ raise
755
+
756
+ # Should never reach here, but just in case
757
+ if last_error:
758
+ raise last_error
759
+ raise RuntimeError("Max retries exceeded")
760
+
761
+ def _execute_tool_with_retry(
762
+ self,
763
+ computer_id: str,
764
+ params: Dict,
765
+ orgo_key: str,
766
+ orgo_url: str,
767
+ console: Console,
768
+ callback: Optional[Callable],
769
+ max_retries: int = 3,
770
+ retry_delay: float = 2.0
771
+ ) -> Any:
772
+ """Execute tool with retry logic for screenshots."""
773
+
774
+ action = params.get("action")
775
+
776
+ # Only retry screenshots, execute other actions directly
777
+ if action != "screenshot":
778
+ return self._execute_tool(computer_id, params, orgo_key, orgo_url, callback)
779
+
780
+ last_error = None
781
+
782
+ for attempt in range(max_retries):
783
+ try:
784
+ return self._execute_tool(computer_id, params, orgo_key, orgo_url, callback)
785
+
786
+ except (ScreenshotError, requests.exceptions.RequestException) as e:
787
+ last_error = e
788
+
789
+ if attempt < max_retries - 1:
790
+ delay = retry_delay * (2 ** attempt) # Exponential backoff
791
+ console.retry(attempt + 1, max_retries, delay)
792
+ time.sleep(delay)
793
+ continue
794
+ else:
795
+ # Return placeholder after all retries exhausted
796
+ logger.error(f"Screenshot failed after {max_retries} attempts: {e}")
797
+ return "Screenshot captured (degraded quality)"
798
+
799
+ except Exception as e:
800
+ # Unexpected errors - don't retry
801
+ raise
802
+
803
+ # Fallback if all retries failed
804
+ if last_error:
805
+ logger.error(f"Screenshot failed: {last_error}")
806
+ return "Screenshot captured (degraded quality)"
807
+
808
+ return "Screenshot captured"
809
+
656
810
  def _execute_tool(self, computer_id: str, params: Dict, orgo_key: str, orgo_url: str, callback: Optional[Callable]) -> Any:
657
811
  """Execute a tool action via Orgo API."""
658
812
 
@@ -662,10 +816,10 @@ class AnthropicProvider:
662
816
 
663
817
  try:
664
818
  # =================================================================
665
- # SCREENSHOT - GET request
819
+ # SCREENSHOT - GET request with validation
666
820
  # =================================================================
667
821
  if action == "screenshot":
668
- r = requests.get(f"{base_url}/screenshot", headers=headers)
822
+ r = requests.get(f"{base_url}/screenshot", headers=headers, timeout=30)
669
823
  r.raise_for_status()
670
824
 
671
825
  data = r.json()
@@ -673,14 +827,21 @@ class AnthropicProvider:
673
827
 
674
828
  if not image_url:
675
829
  logger.error(f"Screenshot API returned no image URL: {data}")
676
- return "Screenshot captured"
830
+ raise ScreenshotError("No image URL in response")
677
831
 
678
- img_r = requests.get(image_url)
832
+ # Fetch the actual image
833
+ img_r = requests.get(image_url, timeout=30)
679
834
  img_r.raise_for_status()
680
835
 
836
+ # Validate image size
681
837
  if len(img_r.content) < 100:
682
838
  logger.error(f"Screenshot image too small: {len(img_r.content)} bytes")
683
- return "Screenshot captured"
839
+ raise ScreenshotError(f"Invalid image size: {len(img_r.content)} bytes")
840
+
841
+ # Validate it's actually an image
842
+ if not img_r.headers.get('content-type', '').startswith('image/'):
843
+ logger.error(f"Invalid content type: {img_r.headers.get('content-type')}")
844
+ raise ScreenshotError("Response is not an image")
684
845
 
685
846
  image_b64 = base64.b64encode(img_r.content).decode()
686
847
 
@@ -793,10 +954,16 @@ class AnthropicProvider:
793
954
  return f"Unknown action: {action}"
794
955
 
795
956
  except requests.exceptions.RequestException as e:
796
- logger.error(f"API request failed for {action}: {e}")
797
- return f"Action {action} completed"
957
+ if action == "screenshot":
958
+ # Re-raise as ScreenshotError for retry logic
959
+ raise ScreenshotError(f"Screenshot request failed: {e}") from e
960
+ else:
961
+ logger.error(f"API request failed for {action}: {e}")
962
+ return f"Action {action} completed"
798
963
  except Exception as e:
799
964
  logger.error(f"Error executing {action}: {e}")
965
+ if action == "screenshot":
966
+ raise ScreenshotError(f"Screenshot processing failed: {e}") from e
800
967
  return f"Action {action} completed"
801
968
 
802
969
  def _prune_screenshots(self, messages: List[Dict], keep: int):
@@ -815,6 +982,7 @@ class AnthropicProvider:
815
982
  if isinstance(item, dict) and item.get("type") == "image":
816
983
  images.append(item)
817
984
 
985
+ # Replace older screenshots with 1x1 transparent PNG
818
986
  for img in images[:-keep]:
819
987
  if "source" in img:
820
988
  img["source"]["data"] = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orgo
3
- Version: 0.0.37
3
+ Version: 0.0.39
4
4
  Summary: Computers for AI agents
5
5
  Author: Orgo Team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes