cycls 0.0.2.68__tar.gz → 0.0.2.70__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: cycls
3
- Version: 0.0.2.68
3
+ Version: 0.0.2.70
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -70,7 +70,7 @@ async def chat(context):
70
70
  if chunk.choices[0].delta.content:
71
71
  yield chunk.choices[0].delta.content
72
72
 
73
- agent.deploy(prod=True) # Live at https://my-agent.cycls.ai
73
+ agent.deploy() # Live at https://my-agent.cycls.ai
74
74
  ```
75
75
 
76
76
  ## Installation
@@ -90,11 +90,12 @@ Requires Docker.
90
90
  - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
91
91
  - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
92
92
 
93
- ## Deploying
93
+ ## Running
94
94
 
95
95
  ```python
96
- agent.deploy(prod=False) # Development: localhost:8080
97
- agent.deploy(prod=True) # Production: https://agent-name.cycls.ai
96
+ agent.local() # Development with hot-reload (localhost:8080)
97
+ agent.local(watch=False) # Development without hot-reload
98
+ agent.deploy() # Production: https://agent-name.cycls.ai
98
99
  ```
99
100
 
100
101
  Get an API key at [cycls.com](https://cycls.com).
@@ -47,7 +47,7 @@ async def chat(context):
47
47
  if chunk.choices[0].delta.content:
48
48
  yield chunk.choices[0].delta.content
49
49
 
50
- agent.deploy(prod=True) # Live at https://my-agent.cycls.ai
50
+ agent.deploy() # Live at https://my-agent.cycls.ai
51
51
  ```
52
52
 
53
53
  ## Installation
@@ -67,11 +67,12 @@ Requires Docker.
67
67
  - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
68
68
  - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
69
69
 
70
- ## Deploying
70
+ ## Running
71
71
 
72
72
  ```python
73
- agent.deploy(prod=False) # Development: localhost:8080
74
- agent.deploy(prod=True) # Production: https://agent-name.cycls.ai
73
+ agent.local() # Development with hot-reload (localhost:8080)
74
+ agent.local(watch=False) # Development without hot-reload
75
+ agent.deploy() # Production: https://agent-name.cycls.ai
75
76
  ```
76
77
 
77
78
  Get an API key at [cycls.com](https://cycls.com).
@@ -9,6 +9,9 @@ from pathlib import Path
9
9
  from contextlib import contextmanager
10
10
  import tarfile
11
11
 
12
+ # Enable BuildKit for faster builds with better caching
13
+ os.environ["DOCKER_BUILDKIT"] = "1"
14
+
12
15
  # --- Top-Level Helper Functions ---
13
16
 
14
17
  def _bootstrap_script(payload_file: str, result_file: str) -> str:
@@ -325,6 +328,73 @@ COPY {self.payload_file} {self.io_dir}/
325
328
  print(f"\n🛑 Operation stopped: {e}")
326
329
  return None
327
330
 
331
+ def watch(self, *args, **kwargs):
332
+ """Runs the container with file watching - restarts script on changes."""
333
+ try:
334
+ from watchfiles import watch as watchfiles_watch
335
+ except ImportError:
336
+ print("❌ watchfiles not installed. Run: pip install watchfiles")
337
+ return
338
+
339
+ import inspect
340
+ import subprocess
341
+
342
+ # Get the main script (the outermost .py file in the stack)
343
+ main_script = None
344
+ for frame_info in inspect.stack():
345
+ filename = frame_info.filename
346
+ if filename.endswith('.py') and not filename.startswith('<'):
347
+ main_script = Path(filename).resolve()
348
+ # main_script is now the outermost/first script in the call chain
349
+
350
+ # Build watch paths: main script + copy sources
351
+ watch_paths = []
352
+ if main_script and main_script.exists():
353
+ watch_paths.append(main_script)
354
+ watch_paths.extend([Path(src).resolve() for src in self.copy.keys() if Path(src).exists()])
355
+
356
+ if not watch_paths:
357
+ print("⚠️ No files to watch. Running without watch mode.")
358
+ return self.run(*args, **kwargs)
359
+
360
+ print(f"👀 Watching for changes:")
361
+ for p in watch_paths:
362
+ print(f" {p}")
363
+ print()
364
+
365
+ while True:
366
+ # Run the script in a subprocess so we survive errors
367
+ print(f"🚀 Running {main_script.name}...")
368
+ proc = subprocess.Popen(
369
+ [sys.executable, str(main_script)],
370
+ env={**os.environ, '_CYCLS_WATCH_CHILD': '1'}
371
+ )
372
+
373
+ try:
374
+ # Watch for changes
375
+ for changes in watchfiles_watch(*watch_paths):
376
+ changed_files = [str(c[1]) for c in changes]
377
+ print(f"\n🔄 Changes detected:")
378
+ for f in changed_files:
379
+ print(f" {f}")
380
+ break
381
+
382
+ print("\n🔄 Restarting...\n")
383
+ proc.terminate()
384
+ try:
385
+ proc.wait(timeout=3)
386
+ except subprocess.TimeoutExpired:
387
+ proc.kill()
388
+
389
+ except KeyboardInterrupt:
390
+ print("\n🛑 Stopping...")
391
+ proc.terminate()
392
+ try:
393
+ proc.wait(timeout=3)
394
+ except subprocess.TimeoutExpired:
395
+ proc.kill()
396
+ return
397
+
328
398
  def build(self, *args, **kwargs):
329
399
  """Builds a self-contained, deployable Docker image locally."""
330
400
  print("📦 Building self-contained image for deployment...")
@@ -1,4 +1,4 @@
1
- import time, inspect, uvicorn
1
+ import os, time, inspect, uvicorn
2
2
  from .runtime import Runtime
3
3
  from .web import web, Config
4
4
  from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
@@ -74,7 +74,8 @@ class Agent:
74
74
  return f
75
75
  return decorator
76
76
 
77
- def local(self, port=8080):
77
+ def _local(self, port=8080, watch=True):
78
+ """Run directly with uvicorn (no Docker)."""
78
79
  if not self.registered_functions:
79
80
  print("Error: No @agent decorated function found.")
80
81
  return
@@ -85,16 +86,14 @@ class Agent:
85
86
  print(f"🚀 Starting local server at localhost:{port}")
86
87
  agent.config.public_path = self.theme
87
88
  set_prod(agent.config, False)
88
- uvicorn.run(web(agent.func, agent.config), host="0.0.0.0", port=port)
89
+ uvicorn.run(web(agent.func, agent.config), host="0.0.0.0", port=port, reload=watch)
89
90
  return
90
91
 
91
- def deploy(self, prod=False, port=8080):
92
+ def _runtime(self, prod=False):
93
+ """Create a Runtime instance for the first registered agent."""
92
94
  if not self.registered_functions:
93
95
  print("Error: No @agent decorated function found.")
94
- return
95
- if (self.key is None) and prod:
96
- print("🛑 Error: Please add your Cycls API key")
97
- return
96
+ return None
98
97
 
99
98
  agent = self.registered_functions[0]
100
99
  if len(self.registered_functions) > 1:
@@ -109,7 +108,7 @@ class Agent:
109
108
  files.update({f: f for f in self.copy})
110
109
  files.update({f: f"public/{f}" for f in self.copy_public})
111
110
 
112
- new = Runtime(
111
+ return Runtime(
113
112
  func=lambda port: __import__("web").serve(func, config_dict, name, port),
114
113
  name=name,
115
114
  apt_packages=self.apt,
@@ -118,8 +117,24 @@ class Agent:
118
117
  base_url=self.base_url,
119
118
  api_key=self.key
120
119
  )
121
- new.deploy(port=port) if prod else new.run(port=port)
122
- return
120
+
121
+ def local(self, port=8080, watch=True):
122
+ """Run locally in Docker with file watching by default."""
123
+ # Child process spawned by watcher - run without watch
124
+ if os.environ.get('_CYCLS_WATCH_CHILD'):
125
+ watch = False
126
+ runtime = self._runtime(prod=False)
127
+ if runtime:
128
+ runtime.watch(port=port) if watch else runtime.run(port=port)
129
+
130
+ def deploy(self, port=8080):
131
+ """Deploy to production."""
132
+ if self.key is None:
133
+ print("🛑 Error: Please add your Cycls API key")
134
+ return
135
+ runtime = self._runtime(prod=True)
136
+ if runtime:
137
+ runtime.deploy(port=port)
123
138
 
124
139
  def modal(self, prod=False):
125
140
  import modal
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.68"
3
+ version = "0.0.2.70"
4
4
 
5
5
  packages = [{ include = "cycls" }]
6
6
  include = ["cycls/theme/**/*"]
File without changes
File without changes
File without changes