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.
- {cycls-0.0.2.81 → cycls-0.0.2.86}/PKG-INFO +20 -20
- {cycls-0.0.2.81 → cycls-0.0.2.86}/README.md +19 -15
- cycls-0.0.2.86/cycls/__init__.py +14 -0
- cycls-0.0.2.86/cycls/app.py +88 -0
- cycls-0.0.2.81/cycls/chat.py → cycls-0.0.2.86/cycls/cli.py +2 -2
- cycls-0.0.2.81/cycls/runtime.py → cycls-0.0.2.86/cycls/function.py +128 -184
- {cycls-0.0.2.81 → cycls-0.0.2.86}/cycls/web.py +19 -5
- {cycls-0.0.2.81 → cycls-0.0.2.86}/pyproject.toml +3 -16
- cycls-0.0.2.81/cycls/__init__.py +0 -20
- cycls-0.0.2.81/cycls/grpc/__init__.py +0 -3
- cycls-0.0.2.81/cycls/grpc/client.py +0 -71
- cycls-0.0.2.81/cycls/grpc/runtime.proto +0 -18
- cycls-0.0.2.81/cycls/grpc/runtime_pb2.py +0 -40
- cycls-0.0.2.81/cycls/grpc/runtime_pb2_grpc.py +0 -100
- cycls-0.0.2.81/cycls/grpc/server.py +0 -60
- cycls-0.0.2.81/cycls/sdk.py +0 -186
- {cycls-0.0.2.81 → cycls-0.0.2.86}/cycls/auth.py +0 -0
- {cycls-0.0.2.81/cycls/default-theme → cycls-0.0.2.86/cycls/themes/default}/assets/index-C2r4Daz3.js +0 -0
- {cycls-0.0.2.81/cycls/default-theme → cycls-0.0.2.86/cycls/themes/default}/assets/index-DWGS8zpa.css +0 -0
- {cycls-0.0.2.81/cycls/default-theme → cycls-0.0.2.86/cycls/themes/default}/index.html +0 -0
- {cycls-0.0.2.81/cycls/dev-theme → cycls-0.0.2.86/cycls/themes/dev}/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cycls
|
|
3
|
-
Version: 0.0.2.
|
|
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
|
-
|
|
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.
|
|
62
|
-
async def
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
113
|
-
async def
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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.
|
|
37
|
-
async def
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
88
|
-
async def
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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-
|
|
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) <
|
|
88
|
+
if len(sys.argv) < 3:
|
|
89
89
|
print("Usage: cycls chat <url|port>")
|
|
90
90
|
sys.exit(1)
|
|
91
|
-
arg = sys.argv[
|
|
91
|
+
arg = sys.argv[2]
|
|
92
92
|
if arg.isdigit():
|
|
93
93
|
port = int(arg)
|
|
94
94
|
if not (1 <= port <= 65535):
|