cycls 0.0.2.69__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.69
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).
@@ -337,6 +337,7 @@ COPY {self.payload_file} {self.io_dir}/
337
337
  return
338
338
 
339
339
  import inspect
340
+ import subprocess
340
341
 
341
342
  # Get the main script (the outermost .py file in the stack)
342
343
  main_script = None
@@ -362,75 +363,37 @@ COPY {self.payload_file} {self.io_dir}/
362
363
  print()
363
364
 
364
365
  while True:
365
- # Run the container in a subprocess-like manner
366
- print(f"🚀 Running function '{self.name}' in container...")
367
- self._perform_auto_cleanup()
368
- self._build_image_if_needed()
369
-
370
- port = kwargs.get('port', None)
371
- ports_mapping = {f'{port}/tcp': port} if port else None
372
-
373
- with tempfile.TemporaryDirectory() as tmpdir_str:
374
- tmpdir = Path(tmpdir_str)
375
- payload_path = tmpdir / self.payload_file
376
-
377
- with payload_path.open('wb') as f:
378
- cloudpickle.dump((self.func, args, kwargs), f)
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
+ )
379
372
 
380
- container = self.docker_client.containers.create(
381
- image=self.tag,
382
- volumes={str(tmpdir): {'bind': self.io_dir, 'mode': 'rw'}},
383
- ports=ports_mapping,
384
- labels={self.managed_label: "true"}
385
- )
386
- container.start()
387
- print(f"✅ Container running on port {port}")
388
- print("👀 Waiting for file changes... (Ctrl+C to stop)\n")
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()
389
388
 
389
+ except KeyboardInterrupt:
390
+ print("\n🛑 Stopping...")
391
+ proc.terminate()
390
392
  try:
391
- # Watch for changes in a separate thread
392
- import threading
393
- import time
394
- change_detected = threading.Event()
395
-
396
- def watch_thread():
397
- for changes in watchfiles_watch(*watch_paths):
398
- changed_files = [str(c[1]) for c in changes]
399
- print(f"\n🔄 Changes detected:")
400
- for f in changed_files:
401
- print(f" {f}")
402
- change_detected.set()
403
- break
404
-
405
- watcher = threading.Thread(target=watch_thread, daemon=True)
406
- watcher.start()
407
-
408
- # Stream container logs in separate thread
409
- def log_thread():
410
- for chunk in container.logs(stream=True, follow=True):
411
- if change_detected.is_set():
412
- break
413
- print(chunk.decode('utf-8').strip())
414
-
415
- logger = threading.Thread(target=log_thread, daemon=True)
416
- logger.start()
417
-
418
- # Wait for change or interrupt
419
- while not change_detected.is_set():
420
- time.sleep(0.5)
421
-
422
- print("\n🔄 Restarting...")
423
- container.stop(timeout=2)
424
- container.remove()
425
- # Re-exec the main script
426
- print(f"🔄 Re-executing {main_script.name}...\n")
427
- os.execv(sys.executable, [sys.executable, str(main_script)])
428
-
429
- except KeyboardInterrupt:
430
- print("\n🛑 Stopping...")
431
- container.stop(timeout=2)
432
- container.remove()
433
- return
393
+ proc.wait(timeout=3)
394
+ except subprocess.TimeoutExpired:
395
+ proc.kill()
396
+ return
434
397
 
435
398
  def build(self, *args, **kwargs):
436
399
  """Builds a self-contained, deployable Docker image locally."""
@@ -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,19 +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, watch=False):
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
98
- if prod and watch:
99
- print("⚠️ Warning: watch=True ignored in production mode.")
100
- watch = False
96
+ return None
101
97
 
102
98
  agent = self.registered_functions[0]
103
99
  if len(self.registered_functions) > 1:
@@ -112,7 +108,7 @@ class Agent:
112
108
  files.update({f: f for f in self.copy})
113
109
  files.update({f: f"public/{f}" for f in self.copy_public})
114
110
 
115
- new = Runtime(
111
+ return Runtime(
116
112
  func=lambda port: __import__("web").serve(func, config_dict, name, port),
117
113
  name=name,
118
114
  apt_packages=self.apt,
@@ -121,13 +117,24 @@ class Agent:
121
117
  base_url=self.base_url,
122
118
  api_key=self.key
123
119
  )
124
- if prod:
125
- new.deploy(port=port)
126
- elif watch:
127
- new.watch(port=port)
128
- else:
129
- new.run(port=port)
130
- 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)
131
138
 
132
139
  def modal(self, prod=False):
133
140
  import modal
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.69"
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