orgo 0.0.37__py3-none-any.whl → 0.0.39__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 +119 -56
- orgo/prompt.py +186 -18
- {orgo-0.0.37.dist-info → orgo-0.0.39.dist-info}/METADATA +1 -1
- {orgo-0.0.37.dist-info → orgo-0.0.39.dist-info}/RECORD +6 -6
- {orgo-0.0.37.dist-info → orgo-0.0.39.dist-info}/WHEEL +0 -0
- {orgo-0.0.37.dist-info → orgo-0.0.39.dist-info}/top_level.txt +0 -0
orgo/computer.py
CHANGED
|
@@ -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(
|
|
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
|
-
#
|
|
33
|
-
computer = Computer(
|
|
53
|
+
# Create computer in new/existing project
|
|
54
|
+
computer = Computer(project="my-project")
|
|
34
55
|
|
|
35
|
-
# Create
|
|
36
|
-
computer = Computer(project="my-project",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
163
|
-
|
|
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
|
-
|
|
228
|
+
if self.verbose:
|
|
229
|
+
logger.warning(f"Failed to resolve image: {e}")
|
|
191
230
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
orgo/prompt.py
CHANGED
|
@@ -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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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.
|
|
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
|
-
|
|
830
|
+
raise ScreenshotError("No image URL in response")
|
|
677
831
|
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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,13 +1,13 @@
|
|
|
1
1
|
orgo/__init__.py,sha256=7uMYbhVNVUtrH7mCd8Ofll36EIdR92LcVlF7bdKuVR4,206
|
|
2
|
-
orgo/computer.py,sha256=
|
|
2
|
+
orgo/computer.py,sha256=7yNxlIk7j6yIcYXDF8HQfJU1uHNHvO9lLP4TTVZEuy4,18661
|
|
3
3
|
orgo/forge.py,sha256=Ey_X3tZwlgHPCSBYBIxPInNaERARoDZQ3vxjvOBLn_I,6714
|
|
4
4
|
orgo/project.py,sha256=Abn58FKAL3vety-MzHJDR-G0GFnEDSD_ljTYnXp9e1I,3148
|
|
5
|
-
orgo/prompt.py,sha256=
|
|
5
|
+
orgo/prompt.py,sha256=EluyrgMLfgQ8C6kR4l_jKhu6mnCuSL14PGYO1AE4FKk,39685
|
|
6
6
|
orgo/api/__init__.py,sha256=9Tzb_OPJ5DH7Cg7OrHzpZZUT4ip05alpa9RLDYmnId8,113
|
|
7
7
|
orgo/api/client.py,sha256=VGdlBCu2gAdDwMZ55n7kQS4R-CFXJjLByXPmRlMLoiY,9097
|
|
8
8
|
orgo/utils/__init__.py,sha256=W4G_nwGBf_7jy0w_mfcrkllurYHSRU4B5cMTVYH_uCc,123
|
|
9
9
|
orgo/utils/auth.py,sha256=tPLBJY-6gdBQWLUjUbwIwxHphC3KoRT_XgP3Iykw3Mw,509
|
|
10
|
-
orgo-0.0.
|
|
11
|
-
orgo-0.0.
|
|
12
|
-
orgo-0.0.
|
|
13
|
-
orgo-0.0.
|
|
10
|
+
orgo-0.0.39.dist-info/METADATA,sha256=VIc2ar6DhP0mQ9RVYrGRlTNPuKO3DUu9kwjJVC_E3cY,894
|
|
11
|
+
orgo-0.0.39.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
orgo-0.0.39.dist-info/top_level.txt,sha256=q0rYtFji8GbYuhFW8A5Ab9e0j27761IKPhnL0E9xow4,5
|
|
13
|
+
orgo-0.0.39.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|