orgo 0.0.11__tar.gz → 0.0.13__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.
- {orgo-0.0.11 → orgo-0.0.13}/PKG-INFO +1 -1
- {orgo-0.0.11 → orgo-0.0.13}/pyproject.toml +1 -1
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/computer.py +25 -30
- orgo-0.0.13/src/orgo/project.py +50 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/prompt.py +56 -27
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo.egg-info/PKG-INFO +1 -1
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo.egg-info/SOURCES.txt +1 -0
- {orgo-0.0.11 → orgo-0.0.13}/README.md +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/setup.cfg +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/__init__.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/adapters/__init__.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/adapters/anthropic.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/adapters/base.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/adapters/openai.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/api/__init__.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/api/client.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/utils/__init__.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo/utils/auth.py +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo.egg-info/dependency_links.txt +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo.egg-info/requires.txt +0 -0
- {orgo-0.0.11 → orgo-0.0.13}/src/orgo.egg-info/top_level.txt +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"""Computer class for interacting with Orgo virtual environments"""
|
|
2
|
-
# src/orgo/computer.py
|
|
3
|
-
|
|
4
2
|
import os
|
|
5
3
|
import io
|
|
6
4
|
import base64
|
|
5
|
+
import logging
|
|
7
6
|
from typing import Dict, List, Any, Optional, Callable, Union
|
|
8
7
|
from PIL import Image
|
|
8
|
+
from requests.exceptions import RequestException
|
|
9
9
|
|
|
10
10
|
from .api.client import ApiClient
|
|
11
11
|
from .prompt import get_provider
|
|
12
|
+
from .project import ProjectManager
|
|
12
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
14
16
|
class Computer:
|
|
15
17
|
def __init__(self, project_id=None, api_key=None, config=None, base_api_url=None):
|
|
@@ -26,16 +28,31 @@ class Computer:
|
|
|
26
28
|
self.base_api_url = base_api_url
|
|
27
29
|
self.api = ApiClient(self.api_key, self.base_api_url)
|
|
28
30
|
|
|
31
|
+
# Look for a saved project ID if none was provided
|
|
32
|
+
if project_id is None:
|
|
33
|
+
project_id = ProjectManager.load_project_id()
|
|
34
|
+
|
|
29
35
|
if project_id:
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
try:
|
|
37
|
+
self.project_id = project_id
|
|
38
|
+
self._info = self.api.connect_computer(project_id)
|
|
39
|
+
except (RequestException, ValueError) as e:
|
|
40
|
+
logger.warning(f"Could not connect to saved project {project_id}: {e}")
|
|
41
|
+
self._create_new_computer(config)
|
|
32
42
|
else:
|
|
33
|
-
|
|
34
|
-
self.project_id = response.get("name")
|
|
35
|
-
self._info = response
|
|
43
|
+
self._create_new_computer(config)
|
|
36
44
|
|
|
45
|
+
def _create_new_computer(self, config=None):
|
|
46
|
+
"""Create a new computer instance and save its ID"""
|
|
47
|
+
response = self.api.create_computer(config)
|
|
48
|
+
self.project_id = response.get("name")
|
|
49
|
+
self._info = response
|
|
50
|
+
|
|
37
51
|
if not self.project_id:
|
|
38
52
|
raise ValueError("Failed to initialize computer: No project ID returned")
|
|
53
|
+
|
|
54
|
+
# Save the project ID for future use
|
|
55
|
+
ProjectManager.save_project_id(self.project_id)
|
|
39
56
|
|
|
40
57
|
def status(self) -> Dict[str, Any]:
|
|
41
58
|
"""Get current computer status"""
|
|
@@ -130,28 +147,6 @@ class Computer:
|
|
|
130
147
|
|
|
131
148
|
Returns:
|
|
132
149
|
List of messages from the conversation
|
|
133
|
-
|
|
134
|
-
Examples:
|
|
135
|
-
# Simple usage with environment variables
|
|
136
|
-
computer.prompt("Open Firefox and search for Python tutorials")
|
|
137
|
-
|
|
138
|
-
# With explicit API key
|
|
139
|
-
computer.prompt("Open Terminal and list files", api_key="your-anthropic-key")
|
|
140
|
-
|
|
141
|
-
# With callback for progress updates
|
|
142
|
-
computer.prompt("Create a new text file", callback=my_callback_function)
|
|
143
|
-
|
|
144
|
-
# With thinking enabled (Claude 3.7 Sonnet)
|
|
145
|
-
computer.prompt(
|
|
146
|
-
"Analyze a complex webpage",
|
|
147
|
-
thinking_enabled=True
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# With custom screenshot management
|
|
151
|
-
computer.prompt(
|
|
152
|
-
"Perform a complex multi-step task",
|
|
153
|
-
max_saved_screenshots=10 # Keep more screenshots for complex tasks
|
|
154
|
-
)
|
|
155
150
|
"""
|
|
156
151
|
# Get the provider instance
|
|
157
152
|
provider_instance = get_provider(provider)
|
|
@@ -173,4 +168,4 @@ class Computer:
|
|
|
173
168
|
# Pass through the Orgo API client configuration
|
|
174
169
|
orgo_api_key=self.api_key,
|
|
175
170
|
orgo_base_url=self.base_api_url
|
|
176
|
-
)
|
|
171
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Project management for Orgo virtual environments"""
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class ProjectManager:
|
|
10
|
+
"""Manages project persistence for Orgo computers"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def load_project_id() -> Optional[str]:
|
|
14
|
+
"""Load project ID from local config file"""
|
|
15
|
+
config_path = ProjectManager._get_config_path()
|
|
16
|
+
|
|
17
|
+
if not os.path.exists(config_path):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
with open(config_path, 'r') as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
return data.get('project_id')
|
|
24
|
+
except (json.JSONDecodeError, IOError, OSError) as e:
|
|
25
|
+
logger.warning(f"Error loading project config: {str(e)}")
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def save_project_id(project_id: str) -> None:
|
|
30
|
+
"""Save project ID to local config file"""
|
|
31
|
+
config_dir = ProjectManager._get_project_dir()
|
|
32
|
+
config_path = ProjectManager._get_config_path()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
36
|
+
with open(config_path, 'w') as f:
|
|
37
|
+
json.dump({'project_id': project_id}, f, indent=2)
|
|
38
|
+
except (IOError, OSError) as e:
|
|
39
|
+
logger.error(f"Failed to save project ID: {str(e)}")
|
|
40
|
+
raise RuntimeError(f"Failed to save project configuration: {str(e)}") from e
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _get_project_dir() -> str:
|
|
44
|
+
"""Get the project directory path"""
|
|
45
|
+
return os.path.join(os.getcwd(), ".orgo")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _get_config_path() -> str:
|
|
49
|
+
"""Get the full path to the config file"""
|
|
50
|
+
return os.path.join(ProjectManager._get_project_dir(), "project.json")
|
|
@@ -3,6 +3,7 @@ Prompt module for interacting with virtual computers using AI models.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import base64
|
|
6
7
|
from typing import Dict, List, Any, Optional, Callable, Union, Protocol
|
|
7
8
|
|
|
8
9
|
|
|
@@ -105,7 +106,14 @@ class AnthropicProvider:
|
|
|
105
106
|
- File browser items: DOUBLE-CLICK to open folders and files
|
|
106
107
|
- When submitting, use the 'Enter' key, not the 'Return' key.
|
|
107
108
|
* If you see an icon on the desktop that you need to open, ALWAYS use the double_click action, never use left_click.
|
|
108
|
-
</UBUNTU_DESKTOP_GUIDELINES>
|
|
109
|
+
</UBUNTU_DESKTOP_GUIDELINES>
|
|
110
|
+
|
|
111
|
+
<SCREENSHOT_GUIDELINES>
|
|
112
|
+
* Be mindful of how many screenshots you take - they consume significant memory.
|
|
113
|
+
* Only take screenshots when you need to see the current state of the screen.
|
|
114
|
+
* Try to batch multiple actions before taking another screenshot.
|
|
115
|
+
* For better performance, limit the number of screenshots you take.
|
|
116
|
+
</SCREENSHOT_GUIDELINES>"""
|
|
109
117
|
|
|
110
118
|
try:
|
|
111
119
|
# Define the computer tool per Anthropic's documentation
|
|
@@ -138,9 +146,9 @@ class AnthropicProvider:
|
|
|
138
146
|
while iteration < max_iterations:
|
|
139
147
|
iteration += 1
|
|
140
148
|
|
|
141
|
-
#
|
|
149
|
+
# Filter to keep only the N most recent screenshots
|
|
142
150
|
if screenshot_count > max_saved_screenshots:
|
|
143
|
-
self.
|
|
151
|
+
self._filter_to_n_most_recent_images(messages, max_saved_screenshots)
|
|
144
152
|
screenshot_count = max_saved_screenshots
|
|
145
153
|
|
|
146
154
|
# Create the request parameters
|
|
@@ -161,7 +169,20 @@ class AnthropicProvider:
|
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
# Create message request to Claude
|
|
164
|
-
|
|
172
|
+
try:
|
|
173
|
+
response = client.beta.messages.create(**request_params)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
if "base64" in str(e).lower():
|
|
176
|
+
# If we get a base64 error, try again after more aggressively filtering images
|
|
177
|
+
if callback:
|
|
178
|
+
callback("error", f"Base64 error detected. Attempting recovery...")
|
|
179
|
+
|
|
180
|
+
# Remove all but the most recent image and try again
|
|
181
|
+
self._filter_to_n_most_recent_images(messages, 1)
|
|
182
|
+
response = client.beta.messages.create(**request_params)
|
|
183
|
+
else:
|
|
184
|
+
# Not a base64 error, re-raise
|
|
185
|
+
raise
|
|
165
186
|
|
|
166
187
|
# Extract the content from the response
|
|
167
188
|
response_content = response.content
|
|
@@ -226,49 +247,57 @@ class AnthropicProvider:
|
|
|
226
247
|
if callback:
|
|
227
248
|
callback("error", str(e))
|
|
228
249
|
raise
|
|
229
|
-
|
|
230
|
-
def
|
|
250
|
+
|
|
251
|
+
def _filter_to_n_most_recent_images(self, messages: List[Dict[str, Any]], max_images: int):
|
|
231
252
|
"""
|
|
232
|
-
|
|
253
|
+
Keep only the N most recent images in the conversation history.
|
|
233
254
|
|
|
234
255
|
Args:
|
|
235
256
|
messages: The conversation history
|
|
236
|
-
|
|
257
|
+
max_images: Maximum number of images to keep
|
|
237
258
|
"""
|
|
238
|
-
|
|
259
|
+
# Find all the image blocks in the conversation history
|
|
260
|
+
image_blocks = []
|
|
239
261
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if messages[i]["role"] != "user":
|
|
262
|
+
for msg_idx, msg in enumerate(messages):
|
|
263
|
+
if msg["role"] != "user":
|
|
243
264
|
continue
|
|
244
265
|
|
|
245
|
-
content =
|
|
266
|
+
content = msg.get("content", [])
|
|
246
267
|
if not isinstance(content, list):
|
|
247
268
|
continue
|
|
248
269
|
|
|
249
|
-
|
|
250
|
-
for j, block in enumerate(content):
|
|
270
|
+
for content_idx, block in enumerate(content):
|
|
251
271
|
if not isinstance(block, dict):
|
|
252
272
|
continue
|
|
253
273
|
|
|
254
274
|
if block.get("type") != "tool_result":
|
|
255
275
|
continue
|
|
256
|
-
|
|
257
|
-
# Check if this tool_result contains an image
|
|
276
|
+
|
|
258
277
|
block_content = block.get("content", [])
|
|
259
|
-
for
|
|
278
|
+
for content_item_idx, content_item in enumerate(block_content):
|
|
260
279
|
if not isinstance(content_item, dict):
|
|
261
280
|
continue
|
|
262
281
|
|
|
263
|
-
if content_item.get("type") == "image":
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
282
|
+
if content_item.get("type") == "image" and "source" in content_item:
|
|
283
|
+
image_blocks.append({
|
|
284
|
+
"msg_idx": msg_idx,
|
|
285
|
+
"content_idx": content_idx,
|
|
286
|
+
"block": block,
|
|
287
|
+
"content_item_idx": content_item_idx,
|
|
288
|
+
"content_item": content_item
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
# If we have more images than our limit, remove the oldest ones
|
|
292
|
+
if len(image_blocks) > max_images:
|
|
293
|
+
# Keep only the most recent ones (which are at the end of the list)
|
|
294
|
+
images_to_remove = image_blocks[:-max_images]
|
|
295
|
+
|
|
296
|
+
for img_block in images_to_remove:
|
|
297
|
+
content_item = img_block["content_item"]
|
|
298
|
+
if "source" in content_item and "data" in content_item["source"]:
|
|
299
|
+
# Replace the base64 data with a placeholder
|
|
300
|
+
content_item["source"]["data"] = "[IMAGE DATA REMOVED]"
|
|
272
301
|
|
|
273
302
|
def _execute_tool(self,
|
|
274
303
|
computer_id: str,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|