orgo 0.0.39__tar.gz → 0.0.41__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.39
3
+ Version: 0.0.41
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.39"
7
+ version = "0.0.41"
8
8
  description = "Computers for AI agents"
9
9
  authors = [{name = "Orgo Team"}]
10
10
  license = {text = "MIT"}
@@ -0,0 +1,12 @@
1
+ # src/orgo/__init__.py
2
+ """Orgo SDK: Desktop infrastructure for AI agents"""
3
+
4
+ from .project import Project
5
+ from .computer import Computer
6
+ from .template import Template
7
+
8
+ # Aliases
9
+ Workspace = Project # Workspace is an alias for Project
10
+ Forge = Template # Forge is deprecated, use Template instead
11
+
12
+ __all__ = ["Project", "Workspace", "Computer", "Template", "Forge"]
@@ -78,12 +78,12 @@ class ApiClient:
78
78
  return self._request("DELETE", f"projects/{project_id}")
79
79
 
80
80
  # Computer methods
81
- def create_computer(self, project_id: str, computer_name: str,
82
- os: str = "linux", ram: int = 2, cpu: int = 2,
81
+ def create_computer(self, project_id: str, computer_name: str,
82
+ os: str = "linux", ram: int = 2, cpu: int = 2,
83
83
  gpu: str = "none", image: Optional[str] = None) -> Dict[str, Any]:
84
- """Create a new computer within a project"""
84
+ """Create a new computer within a workspace/project"""
85
85
  data = {
86
- "project_id": project_id,
86
+ "workspace_id": project_id, # API accepts both workspace_id and project_id
87
87
  "name": computer_name,
88
88
  "os": os,
89
89
  "ram": ram,
@@ -92,7 +92,7 @@ class ApiClient:
92
92
  }
93
93
  if image:
94
94
  data["image"] = image
95
-
95
+
96
96
  return self._request("POST", "computers", data)
97
97
 
98
98
  def list_computers(self, project_id: str) -> List[Dict[str, Any]]:
@@ -211,8 +211,8 @@ class ApiClient:
211
211
  "name": name
212
212
  })
213
213
 
214
- def update_build(self, build_id: str, status: str,
215
- error_message: Optional[str] = None,
214
+ def update_build(self, build_id: str, status: str,
215
+ error_message: Optional[str] = None,
216
216
  build_log: Optional[str] = None) -> Dict[str, Any]:
217
217
  """Update build status"""
218
218
  data = {
@@ -222,6 +222,6 @@ class ApiClient:
222
222
  if error_message:
223
223
  data["errorMessage"] = error_message
224
224
  if build_log:
225
- data["buildLog"] = build_log
226
-
225
+ data["logs"] = build_log
226
+
227
227
  return self._request("POST", "builds/update", data)
@@ -66,24 +66,27 @@ class Computer:
66
66
  computer.prompt("Open Firefox", provider="anthropic")
67
67
  """
68
68
 
69
- def __init__(self,
69
+ def __init__(self,
70
70
  project: Optional[Union[str, 'Project']] = None,
71
+ workspace: Optional[Union[str, 'Project']] = None, # Alias for project
71
72
  name: Optional[str] = None,
72
73
  computer_id: Optional[str] = None,
73
- api_key: Optional[str] = None,
74
+ api_key: Optional[str] = None,
74
75
  base_api_url: Optional[str] = None,
75
76
  ram: Optional[Literal[1, 2, 4, 8, 16, 32, 64]] = None,
76
77
  memory: Optional[Literal[1, 2, 4, 8, 16, 32, 64]] = None,
77
78
  cpu: Optional[Literal[1, 2, 4, 8, 16]] = None,
78
79
  os: Optional[Literal["linux", "windows"]] = None,
79
80
  gpu: Optional[Literal["none", "a10", "l40s", "a100-40gb", "a100-80gb"]] = None,
80
- image: Optional[Union[str, Any]] = None,
81
+ template: Optional[Union[str, Any]] = None,
82
+ image: Optional[Union[str, Any]] = None, # Deprecated, use template
81
83
  verbose: bool = True):
82
84
  """
83
85
  Initialize an Orgo virtual computer.
84
-
86
+
85
87
  Args:
86
- project: Project name or Project instance (creates if doesn't exist)
88
+ project: Project/workspace name or instance (creates if doesn't exist)
89
+ workspace: Alias for project (preferred name going forward)
87
90
  name: Computer name (auto-generated if not provided)
88
91
  computer_id: Connect to existing computer by ID
89
92
  api_key: Orgo API key (defaults to ORGO_API_KEY env var)
@@ -92,9 +95,13 @@ class Computer:
92
95
  cpu: CPU cores (1, 2, 4, 8, 16)
93
96
  os: "linux" or "windows"
94
97
  gpu: "none", "a10", "l40s", "a100-40gb", "a100-80gb"
95
- image: Custom image reference or Forge object
98
+ template: Template name or Template object to use
99
+ image: Deprecated, use template instead
96
100
  verbose: Show console output (default: True)
97
101
  """
102
+ # workspace is an alias for project
103
+ if workspace is not None and project is None:
104
+ project = workspace
98
105
  self.api_key = api_key or operating_system.environ.get("ORGO_API_KEY")
99
106
  self.base_api_url = base_api_url
100
107
  self.api = ApiClient(self.api_key, self.base_api_url)
@@ -102,17 +109,32 @@ class Computer:
102
109
 
103
110
  if ram is None and memory is not None:
104
111
  ram = memory
105
-
112
+
106
113
  self.os = os or "linux"
107
114
  self.ram = ram or 2
108
115
  self.cpu = cpu or 2
109
116
  self.gpu = gpu or "none"
110
- self.image = image
111
-
112
- if hasattr(self.image, 'build') and callable(self.image.build):
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):
113
135
  if self.verbose:
114
- _print_info("Building image from Forge object...")
115
- self.image = self.image.build()
136
+ _print_info("Building image...")
137
+ self.template = self.template.build()
116
138
 
117
139
  if computer_id:
118
140
  self.computer_id = computer_id
@@ -213,20 +235,23 @@ class Computer:
213
235
  if self.gpu not in ["none", "a10", "l40s", "a100-40gb", "a100-80gb"]:
214
236
  raise ValueError("gpu must be: 'none', 'a10', 'l40s', 'a100-40gb', or 'a100-80gb'")
215
237
 
216
- # Resolve image if needed
217
- image_ref = self.image
238
+ # Resolve template to image reference if needed
239
+ image_ref = self.template
218
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
219
242
  try:
220
243
  project_info = self.api.get_project(project_id)
221
- org_id = project_info.get("org_id", "orgo")
244
+ org_id = project_info.get("user_id", "orgo")
222
245
  response = self.api.get_latest_build(org_id, project_id, image_ref)
223
246
  if response and response.get("build"):
224
247
  resolved = response.get("build", {}).get("imageRef")
225
248
  if resolved:
226
249
  image_ref = resolved
250
+ if self.verbose:
251
+ _print_info(f"Using template '{self.template}'")
227
252
  except Exception as e:
228
253
  if self.verbose:
229
- logger.warning(f"Failed to resolve image: {e}")
254
+ logger.warning(f"Template '{image_ref}' not found, using default image")
230
255
 
231
256
  # Create the computer
232
257
  try:
@@ -0,0 +1,347 @@
1
+ """
2
+ Orgo Template - Pre-configure computer environments.
3
+
4
+ Usage:
5
+ from orgo import Template, Computer
6
+
7
+ # Create a template with pre-installed software
8
+ template = Template("python-ml")
9
+ template.run("pip install numpy pandas scikit-learn")
10
+ template.build()
11
+
12
+ # Launch computers from the template
13
+ computer = Computer(template="python-ml")
14
+ """
15
+
16
+ import os
17
+ import subprocess
18
+ import logging
19
+ from typing import Optional, List
20
+
21
+ from .api.client import ApiClient
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _print_info(message: str):
27
+ print(f"-> {message}")
28
+
29
+
30
+ def _print_success(message: str):
31
+ print(f"[ok] {message}")
32
+
33
+
34
+ def _print_error(message: str):
35
+ print(f"[error] {message}")
36
+
37
+
38
+ class Template:
39
+ """
40
+ Create reusable computer templates with pre-installed software.
41
+
42
+ Templates let you define a base environment once and launch multiple
43
+ computers from it. This is useful for:
44
+ - Pre-installing dependencies (Python packages, apt packages, etc.)
45
+ - Cloning repositories
46
+ - Setting up configuration files
47
+
48
+ Examples:
49
+ # Create and build a template
50
+ template = Template("data-science")
51
+ template.run("apt-get update && apt-get install -y python3-pip")
52
+ template.run("pip install numpy pandas matplotlib jupyter")
53
+ template.build()
54
+
55
+ # Launch a computer from the template
56
+ computer = Computer(template="data-science")
57
+
58
+ # Or pass the template object directly
59
+ computer = Computer(template=template)
60
+ """
61
+
62
+ def __init__(self,
63
+ name: str,
64
+ workspace: Optional[str] = None,
65
+ api_key: Optional[str] = None,
66
+ base_api_url: Optional[str] = None,
67
+ verbose: bool = True):
68
+ """
69
+ Initialize a Template.
70
+
71
+ Args:
72
+ name: Template name (used to reference it later)
73
+ workspace: Workspace to store the template in (auto-created if needed)
74
+ api_key: Orgo API key (defaults to ORGO_API_KEY env var)
75
+ base_api_url: Custom API URL
76
+ verbose: Show console output (default: True)
77
+ """
78
+ self.name = name
79
+ self.verbose = verbose
80
+ self.api_key = api_key or os.environ.get("ORGO_API_KEY")
81
+ self.base_api_url = base_api_url
82
+ self.api = ApiClient(self.api_key, self.base_api_url)
83
+
84
+ # Resolve workspace
85
+ workspace_name = workspace or "templates"
86
+ self._resolve_workspace(workspace_name)
87
+
88
+ # Build steps (starts with Orgo base image)
89
+ self._steps: List[str] = ["FROM registry.fly.io/orgo-image-repo:latest"]
90
+ self._image_ref: Optional[str] = None
91
+
92
+ if self.verbose:
93
+ _print_info(f"Template '{name}' initialized")
94
+
95
+ def _resolve_workspace(self, workspace_name: str):
96
+ """Find or create workspace for templates"""
97
+ try:
98
+ workspace = self.api.get_project_by_name(workspace_name)
99
+ self._workspace_id = workspace["id"]
100
+ self._org_id = workspace.get("user_id", "orgo")
101
+ except Exception:
102
+ if self.verbose:
103
+ _print_info(f"Creating workspace '{workspace_name}'...")
104
+ workspace = self.api.create_project(workspace_name)
105
+ self._workspace_id = workspace["id"]
106
+ self._org_id = workspace.get("user_id", "orgo")
107
+
108
+ # =========================================================================
109
+ # Build Steps (Chainable)
110
+ # =========================================================================
111
+
112
+ def run(self, command: str) -> 'Template':
113
+ """
114
+ Run a command during build (like apt-get, pip install, etc.)
115
+
116
+ Args:
117
+ command: Shell command to run
118
+
119
+ Returns:
120
+ Self for chaining
121
+
122
+ Example:
123
+ template.run("apt-get update")
124
+ template.run("pip install requests")
125
+ """
126
+ self._steps.append(f"RUN {command}")
127
+ return self
128
+
129
+ def copy(self, src: str, dest: str) -> 'Template':
130
+ """
131
+ Copy local files into the template.
132
+
133
+ Args:
134
+ src: Local file or directory path
135
+ dest: Destination path in the template
136
+
137
+ Returns:
138
+ Self for chaining
139
+
140
+ Example:
141
+ template.copy("./config.json", "/app/config.json")
142
+ template.copy("./scripts", "/app/scripts")
143
+ """
144
+ self._steps.append(f"COPY {src} {dest}")
145
+ return self
146
+
147
+ def env(self, key: str, value: str) -> 'Template':
148
+ """
149
+ Set an environment variable.
150
+
151
+ Args:
152
+ key: Environment variable name
153
+ value: Environment variable value
154
+
155
+ Returns:
156
+ Self for chaining
157
+
158
+ Example:
159
+ template.env("PYTHONPATH", "/app")
160
+ template.env("DEBUG", "true")
161
+ """
162
+ self._steps.append(f"ENV {key}={value}")
163
+ return self
164
+
165
+ def workdir(self, path: str) -> 'Template':
166
+ """
167
+ Set the working directory.
168
+
169
+ Args:
170
+ path: Directory path
171
+
172
+ Returns:
173
+ Self for chaining
174
+
175
+ Example:
176
+ template.workdir("/app")
177
+ """
178
+ self._steps.append(f"WORKDIR {path}")
179
+ return self
180
+
181
+ def clone(self, repo_url: str, dest: str = "/repo") -> 'Template':
182
+ """
183
+ Clone a git repository.
184
+
185
+ Args:
186
+ repo_url: Git repository URL
187
+ dest: Destination directory (default: /repo)
188
+
189
+ Returns:
190
+ Self for chaining
191
+
192
+ Example:
193
+ template.clone("https://github.com/user/repo.git")
194
+ template.clone("https://github.com/user/repo.git", "/app")
195
+ """
196
+ self._steps.append(f"RUN git clone {repo_url} {dest}")
197
+ return self
198
+
199
+ # =========================================================================
200
+ # Build
201
+ # =========================================================================
202
+
203
+ def build(self, context: str = ".") -> str:
204
+ """
205
+ Build and publish the template.
206
+
207
+ This builds a Docker image with your configuration and pushes it
208
+ to Orgo's registry. After building, you can launch computers from
209
+ this template using Computer(template="name").
210
+
211
+ Args:
212
+ context: Build context directory (default: current directory)
213
+
214
+ Returns:
215
+ Image reference string
216
+
217
+ Example:
218
+ template.run("pip install pandas")
219
+ image_ref = template.build()
220
+ print(f"Built: {image_ref}")
221
+ """
222
+ if len(self._steps) <= 1:
223
+ raise ValueError("No build steps defined. Use .run(), .copy(), etc. first.")
224
+
225
+ if self.verbose:
226
+ _print_info(f"Building template '{self.name}'...")
227
+
228
+ # Register build with API
229
+ try:
230
+ build_info = self.api.create_build(self._org_id, self._workspace_id, self.name)
231
+ build_data = build_info.get('build', {})
232
+
233
+ build_id = build_data.get('id')
234
+ tag = build_data.get('tag')
235
+ image_ref = build_data.get('imageRef')
236
+
237
+ if not build_id or not image_ref:
238
+ raise ValueError("Failed to register build")
239
+
240
+ if self.verbose:
241
+ _print_info(f"Build ID: {build_id}")
242
+ _print_info(f"Image: {image_ref}")
243
+
244
+ except Exception as e:
245
+ _print_error(f"Failed to register build: {e}")
246
+ raise
247
+
248
+ # Generate Dockerfile
249
+ dockerfile_path = f"Dockerfile.{tag}"
250
+ logs = []
251
+
252
+ try:
253
+ with open(dockerfile_path, "w") as f:
254
+ f.write("\n".join(self._steps))
255
+
256
+ # Update status
257
+ self.api.update_build(build_id, "building")
258
+
259
+ # Setup remote builder
260
+ builder_name = "orgo-remote"
261
+ remote_url = "tcp://orgoforge.fly.dev:1234"
262
+
263
+ try:
264
+ subprocess.run(
265
+ ["docker", "buildx", "inspect", builder_name],
266
+ check=True,
267
+ stdout=subprocess.DEVNULL,
268
+ stderr=subprocess.DEVNULL
269
+ )
270
+ except subprocess.CalledProcessError:
271
+ if self.verbose:
272
+ _print_info("Setting up build environment...")
273
+ subprocess.run([
274
+ "docker", "buildx", "create",
275
+ "--name", builder_name,
276
+ "--driver", "remote",
277
+ remote_url,
278
+ "--use"
279
+ ], check=True)
280
+ subprocess.run(["docker", "buildx", "inspect", "--bootstrap"], check=True)
281
+
282
+ # Build and push
283
+ cmd = [
284
+ "docker", "buildx", "build",
285
+ "--builder", builder_name,
286
+ "-t", image_ref,
287
+ "-f", dockerfile_path,
288
+ context,
289
+ "--push"
290
+ ]
291
+
292
+ if self.verbose:
293
+ _print_info("Building image...")
294
+
295
+ process = subprocess.Popen(
296
+ cmd,
297
+ stdout=subprocess.PIPE,
298
+ stderr=subprocess.STDOUT,
299
+ text=True,
300
+ bufsize=1
301
+ )
302
+
303
+ while True:
304
+ line = process.stdout.readline()
305
+ if not line and process.poll() is not None:
306
+ break
307
+ if line:
308
+ if self.verbose:
309
+ print(line, end="")
310
+ logs.append(line)
311
+
312
+ if process.returncode != 0:
313
+ raise Exception(f"Build failed with exit code {process.returncode}")
314
+
315
+ # Success
316
+ self.api.update_build(build_id, "completed", build_log="".join(logs))
317
+ self._image_ref = image_ref
318
+
319
+ if self.verbose:
320
+ _print_success(f"Template '{self.name}' built successfully!")
321
+
322
+ return image_ref
323
+
324
+ except Exception as e:
325
+ _print_error(f"Build failed: {e}")
326
+ try:
327
+ self.api.update_build(build_id, "failed", error_message=str(e), build_log="".join(logs))
328
+ except:
329
+ pass
330
+ raise
331
+
332
+ finally:
333
+ if os.path.exists(dockerfile_path):
334
+ os.remove(dockerfile_path)
335
+
336
+ # =========================================================================
337
+ # Properties
338
+ # =========================================================================
339
+
340
+ @property
341
+ def image_ref(self) -> Optional[str]:
342
+ """Get the built image reference (None if not built yet)."""
343
+ return self._image_ref
344
+
345
+ def __repr__(self):
346
+ status = "built" if self._image_ref else "not built"
347
+ return f"Template(name='{self.name}', status='{status}')"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orgo
3
- Version: 0.0.39
3
+ Version: 0.0.41
4
4
  Summary: Computers for AI agents
5
5
  Author: Orgo Team
6
6
  License: MIT
@@ -5,6 +5,7 @@ src/orgo/computer.py
5
5
  src/orgo/forge.py
6
6
  src/orgo/project.py
7
7
  src/orgo/prompt.py
8
+ src/orgo/template.py
8
9
  src/orgo.egg-info/PKG-INFO
9
10
  src/orgo.egg-info/SOURCES.txt
10
11
  src/orgo.egg-info/dependency_links.txt
@@ -1,8 +0,0 @@
1
- # src/orgo/__init__.py
2
- """Orgo SDK: Desktop infrastructure for AI agents"""
3
-
4
- from .project import Project
5
- from .computer import Computer
6
- from .forge import Forge
7
-
8
- __all__ = ["Project", "Computer", "Forge"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes