cycls 0.0.2.81__tar.gz → 0.0.2.86__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.81
3
+ Version: 0.0.2.86
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -12,14 +12,10 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
- Provides-Extra: modal
16
15
  Requires-Dist: cloudpickle (>=3.1.1,<4.0.0)
17
16
  Requires-Dist: docker (>=7.1.0,<8.0.0)
18
17
  Requires-Dist: fastapi (>=0.111.0,<0.112.0)
19
- Requires-Dist: grpcio (>=1.76.0,<2.0.0)
20
18
  Requires-Dist: httpx (>=0.27.0,<0.28.0)
21
- Requires-Dist: modal (>=1.1.0,<2.0.0) ; extra == "modal"
22
- Requires-Dist: protobuf (>=6.0,<7.0)
23
19
  Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
24
20
  Description-Content-Type: text/markdown
25
21
 
@@ -47,7 +43,11 @@ Distribute Intelligence
47
43
 
48
44
  The open-source SDK for distributing AI agents.
49
45
 
50
- The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
46
+ ```
47
+ Agent extends App (prompts, skills)
48
+ └── App extends Function (web UI)
49
+ └── Function (containerization)
50
+ ```
51
51
 
52
52
  ## Distribute Intelligence
53
53
 
@@ -58,8 +58,8 @@ import cycls
58
58
 
59
59
  cycls.api_key = "YOUR_CYCLS_API_KEY"
60
60
 
61
- @cycls.agent(pip=["openai"])
62
- async def agent(context):
61
+ @cycls.app(pip=["openai"])
62
+ async def app(context):
63
63
  from openai import AsyncOpenAI
64
64
  client = AsyncOpenAI()
65
65
 
@@ -76,7 +76,7 @@ async def agent(context):
76
76
  elif event.type == "response.output_text.delta":
77
77
  yield event.delta
78
78
 
79
- agent.deploy() # Live at https://agent.cycls.ai
79
+ app.deploy() # Live at https://agent.cycls.ai
80
80
  ```
81
81
 
82
82
  ## Installation
@@ -99,9 +99,9 @@ Requires Docker.
99
99
  ## Running
100
100
 
101
101
  ```python
102
- agent.local() # Development with hot-reload (localhost:8080)
103
- agent.local(watch=False) # Development without hot-reload
104
- agent.deploy() # Production: https://agent.cycls.ai
102
+ app.local() # Development with hot-reload (localhost:8080)
103
+ app.local(watch=False) # Development without hot-reload
104
+ app.deploy() # Production: https://agent.cycls.ai
105
105
  ```
106
106
 
107
107
  Get an API key at [cycls.com](https://cycls.com).
@@ -109,8 +109,8 @@ Get an API key at [cycls.com](https://cycls.com).
109
109
  ## Authentication & Analytics
110
110
 
111
111
  ```python
112
- @cycls.agent(pip=["openai"], auth=True, analytics=True)
113
- async def agent(context):
112
+ @cycls.app(pip=["openai"], auth=True, analytics=True)
113
+ async def app(context):
114
114
  # context.user available when auth=True
115
115
  user = context.user # User(id, email, name, plans)
116
116
  yield f"Hello {user.name}!"
@@ -127,7 +127,7 @@ async def agent(context):
127
127
  Yield structured objects for rich streaming responses:
128
128
 
129
129
  ```python
130
- @cycls.agent()
130
+ @cycls.app()
131
131
  async def demo(context):
132
132
  yield {"type": "thinking", "thinking": "Analyzing the request..."}
133
133
  yield "Here's what I found:\n\n"
@@ -169,7 +169,7 @@ This works seamlessly with OpenAI's reasoning models - just map reasoning summar
169
169
  ## Context Object
170
170
 
171
171
  ```python
172
- @cycls.agent()
172
+ @cycls.app()
173
173
  async def chat(context):
174
174
  context.messages # [{"role": "user", "content": "..."}]
175
175
  context.messages.raw # Full data including UI component parts
@@ -202,13 +202,13 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
202
202
  Define your entire runtime in the decorator:
203
203
 
204
204
  ```python
205
- @cycls.agent(
205
+ @cycls.app(
206
206
  pip=["openai", "pandas", "numpy"],
207
207
  apt=["ffmpeg", "libmagic1"],
208
208
  copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
209
209
  copy_public=["./assets/logo.png", "./static/"],
210
210
  )
211
- async def my_agent(context):
211
+ async def my_app(context):
212
212
  ...
213
213
  ```
214
214
 
@@ -243,7 +243,7 @@ copy=[
243
243
  Then import them in your function:
244
244
 
245
245
  ```python
246
- @cycls.agent(copy=["./utils.py"])
246
+ @cycls.app(copy=["./utils.py"])
247
247
  async def chat(context):
248
248
  from utils import helper_function # Your bundled module
249
249
  ...
@@ -257,7 +257,7 @@ Files and directories served at the `/public` endpoint. Perfect for images, down
257
257
  copy_public=["./assets/logo.png", "./downloads/"]
258
258
  ```
259
259
 
260
- Access them at `https://your-agent.cycls.ai/public/logo.png`.
260
+ Access them at `https://your-app.cycls.ai/public/logo.png`.
261
261
 
262
262
  ---
263
263
 
@@ -22,7 +22,11 @@ Distribute Intelligence
22
22
 
23
23
  The open-source SDK for distributing AI agents.
24
24
 
25
- The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
25
+ ```
26
+ Agent extends App (prompts, skills)
27
+ └── App extends Function (web UI)
28
+ └── Function (containerization)
29
+ ```
26
30
 
27
31
  ## Distribute Intelligence
28
32
 
@@ -33,8 +37,8 @@ import cycls
33
37
 
34
38
  cycls.api_key = "YOUR_CYCLS_API_KEY"
35
39
 
36
- @cycls.agent(pip=["openai"])
37
- async def agent(context):
40
+ @cycls.app(pip=["openai"])
41
+ async def app(context):
38
42
  from openai import AsyncOpenAI
39
43
  client = AsyncOpenAI()
40
44
 
@@ -51,7 +55,7 @@ async def agent(context):
51
55
  elif event.type == "response.output_text.delta":
52
56
  yield event.delta
53
57
 
54
- agent.deploy() # Live at https://agent.cycls.ai
58
+ app.deploy() # Live at https://agent.cycls.ai
55
59
  ```
56
60
 
57
61
  ## Installation
@@ -74,9 +78,9 @@ Requires Docker.
74
78
  ## Running
75
79
 
76
80
  ```python
77
- agent.local() # Development with hot-reload (localhost:8080)
78
- agent.local(watch=False) # Development without hot-reload
79
- agent.deploy() # Production: https://agent.cycls.ai
81
+ app.local() # Development with hot-reload (localhost:8080)
82
+ app.local(watch=False) # Development without hot-reload
83
+ app.deploy() # Production: https://agent.cycls.ai
80
84
  ```
81
85
 
82
86
  Get an API key at [cycls.com](https://cycls.com).
@@ -84,8 +88,8 @@ Get an API key at [cycls.com](https://cycls.com).
84
88
  ## Authentication & Analytics
85
89
 
86
90
  ```python
87
- @cycls.agent(pip=["openai"], auth=True, analytics=True)
88
- async def agent(context):
91
+ @cycls.app(pip=["openai"], auth=True, analytics=True)
92
+ async def app(context):
89
93
  # context.user available when auth=True
90
94
  user = context.user # User(id, email, name, plans)
91
95
  yield f"Hello {user.name}!"
@@ -102,7 +106,7 @@ async def agent(context):
102
106
  Yield structured objects for rich streaming responses:
103
107
 
104
108
  ```python
105
- @cycls.agent()
109
+ @cycls.app()
106
110
  async def demo(context):
107
111
  yield {"type": "thinking", "thinking": "Analyzing the request..."}
108
112
  yield "Here's what I found:\n\n"
@@ -144,7 +148,7 @@ This works seamlessly with OpenAI's reasoning models - just map reasoning summar
144
148
  ## Context Object
145
149
 
146
150
  ```python
147
- @cycls.agent()
151
+ @cycls.app()
148
152
  async def chat(context):
149
153
  context.messages # [{"role": "user", "content": "..."}]
150
154
  context.messages.raw # Full data including UI component parts
@@ -177,13 +181,13 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
177
181
  Define your entire runtime in the decorator:
178
182
 
179
183
  ```python
180
- @cycls.agent(
184
+ @cycls.app(
181
185
  pip=["openai", "pandas", "numpy"],
182
186
  apt=["ffmpeg", "libmagic1"],
183
187
  copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
184
188
  copy_public=["./assets/logo.png", "./static/"],
185
189
  )
186
- async def my_agent(context):
190
+ async def my_app(context):
187
191
  ...
188
192
  ```
189
193
 
@@ -218,7 +222,7 @@ copy=[
218
222
  Then import them in your function:
219
223
 
220
224
  ```python
221
- @cycls.agent(copy=["./utils.py"])
225
+ @cycls.app(copy=["./utils.py"])
222
226
  async def chat(context):
223
227
  from utils import helper_function # Your bundled module
224
228
  ...
@@ -232,7 +236,7 @@ Files and directories served at the `/public` endpoint. Perfect for images, down
232
236
  copy_public=["./assets/logo.png", "./downloads/"]
233
237
  ```
234
238
 
235
- Access them at `https://your-agent.cycls.ai/public/logo.png`.
239
+ Access them at `https://your-app.cycls.ai/public/logo.png`.
236
240
 
237
241
  ---
238
242
 
@@ -0,0 +1,14 @@
1
+ from . import function as _function_module
2
+ from .function import function, Function
3
+ from .app import app, App
4
+
5
+ def __getattr__(name):
6
+ if name in ("api_key", "base_url"):
7
+ return getattr(_function_module, name)
8
+ raise AttributeError(f"module 'cycls' has no attribute '{name}'")
9
+
10
+ def __setattr__(name, value):
11
+ if name in ("api_key", "base_url"):
12
+ setattr(_function_module, name, value)
13
+ else:
14
+ raise AttributeError(f"module 'cycls' has no attribute '{name}'")
@@ -0,0 +1,88 @@
1
+ import os
2
+ import uvicorn
3
+ import importlib.resources
4
+
5
+ from .function import Function, _get_api_key, _get_base_url
6
+ from .web import web, Config
7
+
8
+ CYCLS_PATH = importlib.resources.files('cycls')
9
+
10
+ THEMES = ["default", "dev"]
11
+
12
+
13
+ class App(Function):
14
+ """App extends Function with web UI serving capabilities."""
15
+
16
+ def __init__(self, func, name, theme="default", pip=None, apt=None, copy=None, copy_public=None,
17
+ auth=False, org=None, header=None, intro=None, title=None, plan="free", analytics=False):
18
+ if theme not in THEMES:
19
+ raise ValueError(f"Unknown theme: {theme}. Available: {THEMES}")
20
+ self.user_func = func
21
+ self.theme = theme
22
+ self.copy_public = copy_public or []
23
+
24
+ self.config = Config(
25
+ header=header,
26
+ intro=intro,
27
+ title=title,
28
+ auth=auth,
29
+ plan=plan,
30
+ analytics=analytics,
31
+ org=org,
32
+ )
33
+
34
+ # Build files dict for Function (theme is inside cycls/)
35
+ files = {str(CYCLS_PATH): "cycls"}
36
+ files.update({f: f for f in copy or []})
37
+ files.update({f: f"public/{f}" for f in self.copy_public})
38
+
39
+ super().__init__(
40
+ func=func,
41
+ name=name,
42
+ pip=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", "python-dotenv", "docker", *(pip or [])],
43
+ apt=apt,
44
+ copy=files,
45
+ base_url=_get_base_url(),
46
+ api_key=_get_api_key()
47
+ )
48
+
49
+ def __call__(self, *args, **kwargs):
50
+ return self.user_func(*args, **kwargs)
51
+
52
+ def _prepare_func(self, prod):
53
+ self.config.set_prod(prod)
54
+ self.config.public_path = f"cycls/themes/{self.theme}"
55
+ user_func, config, name = self.user_func, self.config, self.name
56
+ self.func = lambda port: __import__("cycls").web.serve(user_func, config, name, port)
57
+
58
+ def _local(self, port=8080):
59
+ """Run directly with uvicorn (no Docker)."""
60
+ print(f"Starting local server at localhost:{port}")
61
+ self.config.public_path = str(CYCLS_PATH.joinpath(f"themes/{self.theme}"))
62
+ self.config.set_prod(False)
63
+ uvicorn.run(web(self.user_func, self.config), host="0.0.0.0", port=port)
64
+
65
+ def local(self, port=8080, watch=True):
66
+ """Run locally in Docker with file watching by default."""
67
+ if os.environ.get('_CYCLS_WATCH'):
68
+ watch = False
69
+ self._prepare_func(prod=False)
70
+ self.watch(port=port) if watch else self.run(port=port)
71
+
72
+ def deploy(self, port=8080):
73
+ """Deploy to production."""
74
+ if self.api_key is None:
75
+ raise RuntimeError("Missing API key. Set cycls.api_key or CYCLS_API_KEY environment variable.")
76
+ self._prepare_func(prod=True)
77
+ return super().deploy(port=port)
78
+
79
+
80
+ def app(name=None, **kwargs):
81
+ """Decorator that transforms a function into a deployable App."""
82
+ if kwargs.get("plan") == "cycls_pass":
83
+ kwargs["auth"] = True
84
+ kwargs["analytics"] = True
85
+
86
+ def decorator(func):
87
+ return App(func=func, name=name or func.__name__, **kwargs)
88
+ return decorator
@@ -85,10 +85,10 @@ def chat(url):
85
85
 
86
86
 
87
87
  def main():
88
- if len(sys.argv) < 2:
88
+ if len(sys.argv) < 3:
89
89
  print("Usage: cycls chat <url|port>")
90
90
  sys.exit(1)
91
- arg = sys.argv[1]
91
+ arg = sys.argv[2]
92
92
  if arg.isdigit():
93
93
  port = int(arg)
94
94
  if not (1 <= port <= 65535):