cycls 0.0.2.72__tar.gz → 0.0.2.74__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.72 → cycls-0.0.2.74}/PKG-INFO +52 -50
- {cycls-0.0.2.72 → cycls-0.0.2.74}/README.md +51 -49
- cycls-0.0.2.74/cycls/__init__.py +20 -0
- cycls-0.0.2.74/cycls/sdk.py +181 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/web.py +1 -1
- {cycls-0.0.2.72 → cycls-0.0.2.74}/pyproject.toml +5 -1
- cycls-0.0.2.72/cycls/__init__.py +0 -2
- cycls-0.0.2.72/cycls/sdk.py +0 -186
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/auth.py +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/cli.py +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/default-theme/assets/index-B0ZKcm_V.css +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/default-theme/assets/index-D5EDcI4J.js +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/default-theme/index.html +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/dev-theme/index.html +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.74}/cycls/runtime.py +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.74
|
|
4
4
|
Summary: Distribute Intelligence
|
|
5
5
|
Author: Mohammed J. AlRujayi
|
|
6
6
|
Author-email: mj@cycls.com
|
|
@@ -32,6 +32,7 @@ Distribute Intelligence
|
|
|
32
32
|
|
|
33
33
|
<h4 align="center">
|
|
34
34
|
<a href="https://pypi.python.org/pypi/cycls"><img src="https://img.shields.io/pypi/v/cycls.svg?label=cycls+pypi&color=blueviolet" alt="cycls Python package on PyPi" /></a>
|
|
35
|
+
<a href="https://github.com/Cycls/cycls/actions/workflows/tests.yml"><img src="https://github.com/Cycls/cycls/actions/workflows/tests.yml/badge.svg" alt="Tests" /></a>
|
|
35
36
|
<a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
|
|
36
37
|
<a href="https://x.com/cyclsai">
|
|
37
38
|
<img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
|
|
@@ -46,31 +47,32 @@ The open-source SDK for distributing AI agents.
|
|
|
46
47
|
|
|
47
48
|
## Distribute Intelligence
|
|
48
49
|
|
|
49
|
-
AI capabilities shouldn't be locked in notebooks or trapped behind months of infrastructure work. Cycls turns your Python functions into production services - complete with APIs, interfaces, auth, and analytics. You focus on the intelligence. Cycls handles the distribution.
|
|
50
|
-
|
|
51
50
|
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
52
51
|
|
|
53
52
|
```python
|
|
54
53
|
import cycls
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
57
56
|
|
|
58
|
-
@agent("
|
|
59
|
-
async def
|
|
57
|
+
@cycls.agent(pip=["openai"])
|
|
58
|
+
async def agent(context):
|
|
60
59
|
from openai import AsyncOpenAI
|
|
61
60
|
client = AsyncOpenAI()
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
model="
|
|
65
|
-
|
|
66
|
-
stream=True
|
|
62
|
+
stream = await client.responses.create(
|
|
63
|
+
model="o3-mini",
|
|
64
|
+
input=context.messages,
|
|
65
|
+
stream=True,
|
|
66
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
async for
|
|
70
|
-
if
|
|
71
|
-
yield
|
|
69
|
+
async for event in stream:
|
|
70
|
+
if event.type == "response.reasoning_summary_text.delta":
|
|
71
|
+
yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
|
|
72
|
+
elif event.type == "response.output_text.delta":
|
|
73
|
+
yield event.delta
|
|
72
74
|
|
|
73
|
-
agent.deploy() # Live at https://
|
|
75
|
+
agent.deploy() # Live at https://agent.cycls.ai
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
## Installation
|
|
@@ -87,7 +89,7 @@ Requires Docker.
|
|
|
87
89
|
- **Web Interface** - Chat UI served automatically
|
|
88
90
|
- **Authentication** - `auth=True` enables JWT-based access control
|
|
89
91
|
- **Analytics** - `analytics=True` tracks usage
|
|
90
|
-
- **Monetization** - `
|
|
92
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
91
93
|
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
92
94
|
|
|
93
95
|
## Running
|
|
@@ -95,17 +97,33 @@ Requires Docker.
|
|
|
95
97
|
```python
|
|
96
98
|
agent.local() # Development with hot-reload (localhost:8080)
|
|
97
99
|
agent.local(watch=False) # Development without hot-reload
|
|
98
|
-
agent.deploy() # Production: https://agent
|
|
100
|
+
agent.deploy() # Production: https://agent.cycls.ai
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
Get an API key at [cycls.com](https://cycls.com).
|
|
102
104
|
|
|
105
|
+
## Authentication & Analytics
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
@cycls.agent(pip=["openai"], auth=True, analytics=True)
|
|
109
|
+
async def agent(context):
|
|
110
|
+
# context.user available when auth=True
|
|
111
|
+
user = context.user # User(id, email, name, plans)
|
|
112
|
+
yield f"Hello {user.name}!"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Flag | Description |
|
|
116
|
+
|------|-------------|
|
|
117
|
+
| `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
|
|
118
|
+
| `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
|
|
119
|
+
| `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
|
|
120
|
+
|
|
103
121
|
## Native UI Components
|
|
104
122
|
|
|
105
123
|
Yield structured objects for rich streaming responses:
|
|
106
124
|
|
|
107
125
|
```python
|
|
108
|
-
@agent()
|
|
126
|
+
@cycls.agent()
|
|
109
127
|
async def demo(context):
|
|
110
128
|
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
111
129
|
yield "Here's what I found:\n\n"
|
|
@@ -128,32 +146,26 @@ async def demo(context):
|
|
|
128
146
|
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
129
147
|
| `{"type": "image", "src": "..."}` | Yes |
|
|
130
148
|
|
|
131
|
-
###
|
|
149
|
+
### Thinking Bubbles
|
|
132
150
|
|
|
133
|
-
|
|
134
|
-
@agent()
|
|
135
|
-
async def chat(context):
|
|
136
|
-
from openai import AsyncOpenAI
|
|
137
|
-
client = AsyncOpenAI()
|
|
151
|
+
The `{"type": "thinking", "thinking": "..."}` component renders as a collapsible thinking bubble in the UI. Each yield appends to the same bubble until a different component type is yielded:
|
|
138
152
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
153
|
+
```python
|
|
154
|
+
# Multiple yields build one thinking bubble
|
|
155
|
+
yield {"type": "thinking", "thinking": "Let me "}
|
|
156
|
+
yield {"type": "thinking", "thinking": "analyze this..."}
|
|
157
|
+
yield {"type": "thinking", "thinking": " Done thinking."}
|
|
145
158
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
yield {"type": "thinking", "thinking": event.delta}
|
|
149
|
-
elif event.type == "response.output_text.delta":
|
|
150
|
-
yield event.delta
|
|
159
|
+
# Then output the response
|
|
160
|
+
yield "Here's what I found..."
|
|
151
161
|
```
|
|
152
162
|
|
|
163
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
164
|
+
|
|
153
165
|
## Context Object
|
|
154
166
|
|
|
155
167
|
```python
|
|
156
|
-
@agent()
|
|
168
|
+
@cycls.agent()
|
|
157
169
|
async def chat(context):
|
|
158
170
|
context.messages # [{"role": "user", "content": "..."}]
|
|
159
171
|
context.messages.raw # Full data including UI component parts
|
|
@@ -183,16 +195,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
|
|
|
183
195
|
|
|
184
196
|
## Declarative Infrastructure
|
|
185
197
|
|
|
186
|
-
Define your entire runtime in
|
|
198
|
+
Define your entire runtime in the decorator:
|
|
187
199
|
|
|
188
200
|
```python
|
|
189
|
-
|
|
201
|
+
@cycls.agent(
|
|
190
202
|
pip=["openai", "pandas", "numpy"],
|
|
191
203
|
apt=["ffmpeg", "libmagic1"],
|
|
192
|
-
run_commands=["curl -sSL https://example.com/setup.sh | bash"],
|
|
193
204
|
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
194
205
|
copy_public=["./assets/logo.png", "./static/"],
|
|
195
206
|
)
|
|
207
|
+
async def my_agent(context):
|
|
208
|
+
...
|
|
196
209
|
```
|
|
197
210
|
|
|
198
211
|
### `pip` - Python Packages
|
|
@@ -211,17 +224,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
|
|
|
211
224
|
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
212
225
|
```
|
|
213
226
|
|
|
214
|
-
### `run_commands` - Shell Commands
|
|
215
|
-
|
|
216
|
-
Run arbitrary shell commands during the container build. Useful for custom setup scripts, downloading assets, or any build-time configuration.
|
|
217
|
-
|
|
218
|
-
```python
|
|
219
|
-
run_commands=[
|
|
220
|
-
"curl -sSL https://example.com/setup.sh | bash",
|
|
221
|
-
"chmod +x /app/scripts/*.sh"
|
|
222
|
-
]
|
|
223
|
-
```
|
|
224
|
-
|
|
225
227
|
### `copy` - Bundle Files and Directories
|
|
226
228
|
|
|
227
229
|
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
@@ -237,7 +239,7 @@ copy=[
|
|
|
237
239
|
Then import them in your function:
|
|
238
240
|
|
|
239
241
|
```python
|
|
240
|
-
@agent()
|
|
242
|
+
@cycls.agent(copy=["./utils.py"])
|
|
241
243
|
async def chat(context):
|
|
242
244
|
from utils import helper_function # Your bundled module
|
|
243
245
|
...
|
|
@@ -9,6 +9,7 @@ Distribute Intelligence
|
|
|
9
9
|
|
|
10
10
|
<h4 align="center">
|
|
11
11
|
<a href="https://pypi.python.org/pypi/cycls"><img src="https://img.shields.io/pypi/v/cycls.svg?label=cycls+pypi&color=blueviolet" alt="cycls Python package on PyPi" /></a>
|
|
12
|
+
<a href="https://github.com/Cycls/cycls/actions/workflows/tests.yml"><img src="https://github.com/Cycls/cycls/actions/workflows/tests.yml/badge.svg" alt="Tests" /></a>
|
|
12
13
|
<a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
|
|
13
14
|
<a href="https://x.com/cyclsai">
|
|
14
15
|
<img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
|
|
@@ -23,31 +24,32 @@ The open-source SDK for distributing AI agents.
|
|
|
23
24
|
|
|
24
25
|
## Distribute Intelligence
|
|
25
26
|
|
|
26
|
-
AI capabilities shouldn't be locked in notebooks or trapped behind months of infrastructure work. Cycls turns your Python functions into production services - complete with APIs, interfaces, auth, and analytics. You focus on the intelligence. Cycls handles the distribution.
|
|
27
|
-
|
|
28
27
|
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
29
28
|
|
|
30
29
|
```python
|
|
31
30
|
import cycls
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
34
33
|
|
|
35
|
-
@agent("
|
|
36
|
-
async def
|
|
34
|
+
@cycls.agent(pip=["openai"])
|
|
35
|
+
async def agent(context):
|
|
37
36
|
from openai import AsyncOpenAI
|
|
38
37
|
client = AsyncOpenAI()
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
model="
|
|
42
|
-
|
|
43
|
-
stream=True
|
|
39
|
+
stream = await client.responses.create(
|
|
40
|
+
model="o3-mini",
|
|
41
|
+
input=context.messages,
|
|
42
|
+
stream=True,
|
|
43
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
async for
|
|
47
|
-
if
|
|
48
|
-
yield
|
|
46
|
+
async for event in stream:
|
|
47
|
+
if event.type == "response.reasoning_summary_text.delta":
|
|
48
|
+
yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
|
|
49
|
+
elif event.type == "response.output_text.delta":
|
|
50
|
+
yield event.delta
|
|
49
51
|
|
|
50
|
-
agent.deploy() # Live at https://
|
|
52
|
+
agent.deploy() # Live at https://agent.cycls.ai
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
## Installation
|
|
@@ -64,7 +66,7 @@ Requires Docker.
|
|
|
64
66
|
- **Web Interface** - Chat UI served automatically
|
|
65
67
|
- **Authentication** - `auth=True` enables JWT-based access control
|
|
66
68
|
- **Analytics** - `analytics=True` tracks usage
|
|
67
|
-
- **Monetization** - `
|
|
69
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
68
70
|
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
69
71
|
|
|
70
72
|
## Running
|
|
@@ -72,17 +74,33 @@ Requires Docker.
|
|
|
72
74
|
```python
|
|
73
75
|
agent.local() # Development with hot-reload (localhost:8080)
|
|
74
76
|
agent.local(watch=False) # Development without hot-reload
|
|
75
|
-
agent.deploy() # Production: https://agent
|
|
77
|
+
agent.deploy() # Production: https://agent.cycls.ai
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
Get an API key at [cycls.com](https://cycls.com).
|
|
79
81
|
|
|
82
|
+
## Authentication & Analytics
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
@cycls.agent(pip=["openai"], auth=True, analytics=True)
|
|
86
|
+
async def agent(context):
|
|
87
|
+
# context.user available when auth=True
|
|
88
|
+
user = context.user # User(id, email, name, plans)
|
|
89
|
+
yield f"Hello {user.name}!"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Flag | Description |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
|
|
95
|
+
| `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
|
|
96
|
+
| `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
|
|
97
|
+
|
|
80
98
|
## Native UI Components
|
|
81
99
|
|
|
82
100
|
Yield structured objects for rich streaming responses:
|
|
83
101
|
|
|
84
102
|
```python
|
|
85
|
-
@agent()
|
|
103
|
+
@cycls.agent()
|
|
86
104
|
async def demo(context):
|
|
87
105
|
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
88
106
|
yield "Here's what I found:\n\n"
|
|
@@ -105,32 +123,26 @@ async def demo(context):
|
|
|
105
123
|
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
106
124
|
| `{"type": "image", "src": "..."}` | Yes |
|
|
107
125
|
|
|
108
|
-
###
|
|
126
|
+
### Thinking Bubbles
|
|
109
127
|
|
|
110
|
-
|
|
111
|
-
@agent()
|
|
112
|
-
async def chat(context):
|
|
113
|
-
from openai import AsyncOpenAI
|
|
114
|
-
client = AsyncOpenAI()
|
|
128
|
+
The `{"type": "thinking", "thinking": "..."}` component renders as a collapsible thinking bubble in the UI. Each yield appends to the same bubble until a different component type is yielded:
|
|
115
129
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
130
|
+
```python
|
|
131
|
+
# Multiple yields build one thinking bubble
|
|
132
|
+
yield {"type": "thinking", "thinking": "Let me "}
|
|
133
|
+
yield {"type": "thinking", "thinking": "analyze this..."}
|
|
134
|
+
yield {"type": "thinking", "thinking": " Done thinking."}
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
yield {"type": "thinking", "thinking": event.delta}
|
|
126
|
-
elif event.type == "response.output_text.delta":
|
|
127
|
-
yield event.delta
|
|
136
|
+
# Then output the response
|
|
137
|
+
yield "Here's what I found..."
|
|
128
138
|
```
|
|
129
139
|
|
|
140
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
141
|
+
|
|
130
142
|
## Context Object
|
|
131
143
|
|
|
132
144
|
```python
|
|
133
|
-
@agent()
|
|
145
|
+
@cycls.agent()
|
|
134
146
|
async def chat(context):
|
|
135
147
|
context.messages # [{"role": "user", "content": "..."}]
|
|
136
148
|
context.messages.raw # Full data including UI component parts
|
|
@@ -160,16 +172,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
|
|
|
160
172
|
|
|
161
173
|
## Declarative Infrastructure
|
|
162
174
|
|
|
163
|
-
Define your entire runtime in
|
|
175
|
+
Define your entire runtime in the decorator:
|
|
164
176
|
|
|
165
177
|
```python
|
|
166
|
-
|
|
178
|
+
@cycls.agent(
|
|
167
179
|
pip=["openai", "pandas", "numpy"],
|
|
168
180
|
apt=["ffmpeg", "libmagic1"],
|
|
169
|
-
run_commands=["curl -sSL https://example.com/setup.sh | bash"],
|
|
170
181
|
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
171
182
|
copy_public=["./assets/logo.png", "./static/"],
|
|
172
183
|
)
|
|
184
|
+
async def my_agent(context):
|
|
185
|
+
...
|
|
173
186
|
```
|
|
174
187
|
|
|
175
188
|
### `pip` - Python Packages
|
|
@@ -188,17 +201,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
|
|
|
188
201
|
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
189
202
|
```
|
|
190
203
|
|
|
191
|
-
### `run_commands` - Shell Commands
|
|
192
|
-
|
|
193
|
-
Run arbitrary shell commands during the container build. Useful for custom setup scripts, downloading assets, or any build-time configuration.
|
|
194
|
-
|
|
195
|
-
```python
|
|
196
|
-
run_commands=[
|
|
197
|
-
"curl -sSL https://example.com/setup.sh | bash",
|
|
198
|
-
"chmod +x /app/scripts/*.sh"
|
|
199
|
-
]
|
|
200
|
-
```
|
|
201
|
-
|
|
202
204
|
### `copy` - Bundle Files and Directories
|
|
203
205
|
|
|
204
206
|
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
@@ -214,7 +216,7 @@ copy=[
|
|
|
214
216
|
Then import them in your function:
|
|
215
217
|
|
|
216
218
|
```python
|
|
217
|
-
@agent()
|
|
219
|
+
@cycls.agent(copy=["./utils.py"])
|
|
218
220
|
async def chat(context):
|
|
219
221
|
from utils import helper_function # Your bundled module
|
|
220
222
|
...
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
from .sdk import function, agent
|
|
4
|
+
from .runtime import Runtime
|
|
5
|
+
|
|
6
|
+
class _Module(ModuleType):
|
|
7
|
+
def __getattr__(self, name):
|
|
8
|
+
from . import sdk
|
|
9
|
+
if name in ("api_key", "base_url"):
|
|
10
|
+
return getattr(sdk, name)
|
|
11
|
+
raise AttributeError(f"module 'cycls' has no attribute '{name}'")
|
|
12
|
+
|
|
13
|
+
def __setattr__(self, name, value):
|
|
14
|
+
from . import sdk
|
|
15
|
+
if name in ("api_key", "base_url"):
|
|
16
|
+
setattr(sdk, name, value)
|
|
17
|
+
return
|
|
18
|
+
super().__setattr__(name, value)
|
|
19
|
+
|
|
20
|
+
sys.modules[__name__].__class__ = _Module
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import os, time, uvicorn
|
|
2
|
+
from .runtime import Runtime
|
|
3
|
+
from .web import web, Config
|
|
4
|
+
from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
|
|
5
|
+
import importlib.resources
|
|
6
|
+
|
|
7
|
+
CYCLS_PATH = importlib.resources.files('cycls')
|
|
8
|
+
|
|
9
|
+
# Module-level configuration
|
|
10
|
+
api_key = None
|
|
11
|
+
base_url = None
|
|
12
|
+
|
|
13
|
+
themes = {
|
|
14
|
+
"default": CYCLS_PATH.joinpath('default-theme'),
|
|
15
|
+
"dev": CYCLS_PATH.joinpath('dev-theme'),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def _resolve_theme(theme):
|
|
19
|
+
"""Resolve theme - accepts string name or path"""
|
|
20
|
+
if isinstance(theme, str):
|
|
21
|
+
if theme in themes:
|
|
22
|
+
return themes[theme]
|
|
23
|
+
raise ValueError(f"Unknown theme: {theme}. Available: {list(themes.keys())}")
|
|
24
|
+
return theme
|
|
25
|
+
|
|
26
|
+
def _set_prod(config: Config, prod: bool):
|
|
27
|
+
config.prod = prod
|
|
28
|
+
config.pk = PK_LIVE if prod else PK_TEST
|
|
29
|
+
config.jwks = JWKS_PROD if prod else JWKS_TEST
|
|
30
|
+
|
|
31
|
+
class AgentRuntime:
|
|
32
|
+
"""Wraps an agent function with local/deploy/modal capabilities."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, func, name, theme, pip, apt, copy, copy_public, modal_keys, auth, org, domain, header, intro, title, plan, analytics):
|
|
35
|
+
self.func = func
|
|
36
|
+
self.name = name
|
|
37
|
+
self.theme = _resolve_theme(theme)
|
|
38
|
+
self.pip = pip
|
|
39
|
+
self.apt = apt
|
|
40
|
+
self.copy = copy
|
|
41
|
+
self.copy_public = copy_public
|
|
42
|
+
self.modal_keys = modal_keys
|
|
43
|
+
self.domain = domain or f"{name}.cycls.ai"
|
|
44
|
+
|
|
45
|
+
self.config = Config(
|
|
46
|
+
header=header,
|
|
47
|
+
intro=intro,
|
|
48
|
+
title=title,
|
|
49
|
+
auth=auth,
|
|
50
|
+
plan=plan,
|
|
51
|
+
analytics=analytics,
|
|
52
|
+
org=org,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __call__(self, *args, **kwargs):
|
|
56
|
+
"""Make the runtime callable - delegates to the wrapped function."""
|
|
57
|
+
return self.func(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def _local(self, port=8080, watch=True):
|
|
60
|
+
"""Run directly with uvicorn (no Docker)."""
|
|
61
|
+
print(f"Starting local server at localhost:{port}")
|
|
62
|
+
self.config.public_path = self.theme
|
|
63
|
+
_set_prod(self.config, False)
|
|
64
|
+
uvicorn.run(web(self.func, self.config), host="0.0.0.0", port=port, reload=watch)
|
|
65
|
+
|
|
66
|
+
def _runtime(self, prod=False):
|
|
67
|
+
"""Create a Runtime instance for deployment."""
|
|
68
|
+
_set_prod(self.config, prod)
|
|
69
|
+
config_dict = self.config.model_dump()
|
|
70
|
+
|
|
71
|
+
# Extract to local variables to avoid capturing self in lambda (cloudpickle issue)
|
|
72
|
+
func = self.func
|
|
73
|
+
name = self.name
|
|
74
|
+
|
|
75
|
+
files = {str(self.theme): "theme", str(CYCLS_PATH)+"/web.py": "web.py"}
|
|
76
|
+
files.update({f: f for f in self.copy})
|
|
77
|
+
files.update({f: f"public/{f}" for f in self.copy_public})
|
|
78
|
+
|
|
79
|
+
return Runtime(
|
|
80
|
+
func=lambda port: __import__("web").serve(func, config_dict, name, port),
|
|
81
|
+
name=name,
|
|
82
|
+
apt_packages=self.apt,
|
|
83
|
+
pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
|
|
84
|
+
copy=files,
|
|
85
|
+
base_url=base_url,
|
|
86
|
+
api_key=api_key
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def local(self, port=8080, watch=True):
|
|
90
|
+
"""Run locally in Docker with file watching by default."""
|
|
91
|
+
if os.environ.get('_CYCLS_WATCH_CHILD'):
|
|
92
|
+
watch = False
|
|
93
|
+
runtime = self._runtime(prod=False)
|
|
94
|
+
runtime.watch(port=port) if watch else runtime.run(port=port)
|
|
95
|
+
|
|
96
|
+
def deploy(self, port=8080):
|
|
97
|
+
"""Deploy to production."""
|
|
98
|
+
if api_key is None:
|
|
99
|
+
print("Error: Please set cycls.api_key")
|
|
100
|
+
return
|
|
101
|
+
runtime = self._runtime(prod=True)
|
|
102
|
+
runtime.deploy(port=port)
|
|
103
|
+
|
|
104
|
+
def modal(self, prod=False):
|
|
105
|
+
import modal
|
|
106
|
+
from modal.runner import run_app
|
|
107
|
+
|
|
108
|
+
# Extract to local variables to avoid capturing self in lambda
|
|
109
|
+
func = self.func
|
|
110
|
+
name = self.name
|
|
111
|
+
domain = self.domain
|
|
112
|
+
|
|
113
|
+
client = modal.Client.from_credentials(*self.modal_keys)
|
|
114
|
+
image = (modal.Image.debian_slim()
|
|
115
|
+
.pip_install("fastapi[standard]", "pyjwt", "cryptography", *self.pip)
|
|
116
|
+
.apt_install(*self.apt)
|
|
117
|
+
.add_local_dir(self.theme, "/root/theme")
|
|
118
|
+
.add_local_file(str(CYCLS_PATH)+"/web.py", "/root/web.py"))
|
|
119
|
+
|
|
120
|
+
for item in self.copy:
|
|
121
|
+
image = image.add_local_file(item, f"/root/{item}") if "." in item else image.add_local_dir(item, f'/root/{item}')
|
|
122
|
+
|
|
123
|
+
for item in self.copy_public:
|
|
124
|
+
image = image.add_local_file(item, f"/root/public/{item}") if "." in item else image.add_local_dir(item, f'/root/public/{item}')
|
|
125
|
+
|
|
126
|
+
app = modal.App("development", image=image)
|
|
127
|
+
|
|
128
|
+
_set_prod(self.config, prod)
|
|
129
|
+
config_dict = self.config.model_dump()
|
|
130
|
+
|
|
131
|
+
app.function(serialized=True, name=name)(
|
|
132
|
+
modal.asgi_app(label=name, custom_domains=[domain])
|
|
133
|
+
(lambda: __import__("web").web(func, config_dict))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if prod:
|
|
137
|
+
print(f"Deployed to => https://{domain}")
|
|
138
|
+
app.deploy(client=client, name=name)
|
|
139
|
+
else:
|
|
140
|
+
with modal.enable_output():
|
|
141
|
+
run_app(app=app, client=client)
|
|
142
|
+
print("Modal development server is running. Press Ctrl+C to stop.")
|
|
143
|
+
with modal.enable_output(), run_app(app=app, client=client):
|
|
144
|
+
while True: time.sleep(10)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def agent(name=None, pip=[], apt=[], copy=[], copy_public=[], theme="default", modal_keys=["", ""], auth=False, org=None, domain=None, header="", intro="", title="", plan="free", analytics=False):
|
|
148
|
+
"""Decorator that transforms a function into a deployable agent."""
|
|
149
|
+
if plan == "cycls_pass":
|
|
150
|
+
auth = True
|
|
151
|
+
analytics = True
|
|
152
|
+
|
|
153
|
+
def decorator(func):
|
|
154
|
+
agent_name = name or func.__name__.replace('_', '-')
|
|
155
|
+
return AgentRuntime(
|
|
156
|
+
func=func,
|
|
157
|
+
name=agent_name,
|
|
158
|
+
theme=theme,
|
|
159
|
+
pip=pip,
|
|
160
|
+
apt=apt,
|
|
161
|
+
copy=copy,
|
|
162
|
+
copy_public=copy_public,
|
|
163
|
+
modal_keys=modal_keys,
|
|
164
|
+
auth=auth,
|
|
165
|
+
org=org,
|
|
166
|
+
domain=domain,
|
|
167
|
+
header=header,
|
|
168
|
+
intro=intro,
|
|
169
|
+
title=title,
|
|
170
|
+
plan=plan,
|
|
171
|
+
analytics=analytics,
|
|
172
|
+
)
|
|
173
|
+
return decorator
|
|
174
|
+
|
|
175
|
+
def function(python_version=None, pip=None, apt=None, run_commands=None, copy=None, name=None):
|
|
176
|
+
"""Decorator that transforms a Python function into a containerized, remotely executable object."""
|
|
177
|
+
def decorator(func):
|
|
178
|
+
func_name = name or func.__name__
|
|
179
|
+
copy_dict = {i: i for i in copy or []}
|
|
180
|
+
return Runtime(func, func_name.replace('_', '-'), python_version, pip, apt, run_commands, copy_dict, base_url, api_key)
|
|
181
|
+
return decorator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cycls"
|
|
3
|
-
version = "0.0.2.
|
|
3
|
+
version = "0.0.2.74"
|
|
4
4
|
|
|
5
5
|
packages = [{ include = "cycls" }]
|
|
6
6
|
include = ["cycls/theme/**/*"]
|
|
@@ -19,6 +19,10 @@ cloudpickle = "^3.1.1"
|
|
|
19
19
|
[tool.poetry.scripts]
|
|
20
20
|
cycls = "cycls.cli:main"
|
|
21
21
|
|
|
22
|
+
[tool.poetry.group.test.dependencies]
|
|
23
|
+
pytest = "^8.0.0"
|
|
24
|
+
requests = "^2.31.0"
|
|
25
|
+
|
|
22
26
|
[tool.poetry.extras]
|
|
23
27
|
modal = ["modal"]
|
|
24
28
|
|
cycls-0.0.2.72/cycls/__init__.py
DELETED
cycls-0.0.2.72/cycls/sdk.py
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import os, time, inspect, uvicorn
|
|
2
|
-
from .runtime import Runtime
|
|
3
|
-
from .web import web, Config
|
|
4
|
-
from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
|
|
5
|
-
import importlib.resources
|
|
6
|
-
from pydantic import BaseModel
|
|
7
|
-
from typing import Callable
|
|
8
|
-
|
|
9
|
-
CYCLS_PATH = importlib.resources.files('cycls')
|
|
10
|
-
|
|
11
|
-
class RegisteredAgent(BaseModel):
|
|
12
|
-
func: Callable
|
|
13
|
-
name: str
|
|
14
|
-
domain: str
|
|
15
|
-
config: Config
|
|
16
|
-
|
|
17
|
-
def set_prod(config: Config, prod: bool):
|
|
18
|
-
config.prod = prod
|
|
19
|
-
config.pk = PK_LIVE if prod else PK_TEST
|
|
20
|
-
config.jwks = JWKS_PROD if prod else JWKS_TEST
|
|
21
|
-
|
|
22
|
-
themes = {
|
|
23
|
-
"default": CYCLS_PATH.joinpath('default-theme'),
|
|
24
|
-
"dev": CYCLS_PATH.joinpath('dev-theme'),
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
def resolve_theme(theme):
|
|
28
|
-
"""Resolve theme - accepts string name or path"""
|
|
29
|
-
if isinstance(theme, str):
|
|
30
|
-
if theme in themes:
|
|
31
|
-
return themes[theme]
|
|
32
|
-
raise ValueError(f"Unknown theme: {theme}. Available: {list(themes.keys())}")
|
|
33
|
-
return theme
|
|
34
|
-
|
|
35
|
-
def function(python_version=None, pip=None, apt=None, run_commands=None, copy=None, name=None, base_url=None, key=None):
|
|
36
|
-
# """
|
|
37
|
-
# A decorator factory that transforms a Python function into a containerized,
|
|
38
|
-
# remotely executable object.
|
|
39
|
-
def decorator(func):
|
|
40
|
-
Name = name or func.__name__
|
|
41
|
-
copy_dict = {i:i for i in copy or []}
|
|
42
|
-
return Runtime(func, Name.replace('_', '-'), python_version, pip, apt, run_commands, copy_dict, base_url, key)
|
|
43
|
-
return decorator
|
|
44
|
-
|
|
45
|
-
class Agent:
|
|
46
|
-
def __init__(self, theme="default", org=None, api_token=None, pip=[], apt=[], copy=[], copy_public=[], modal_keys=["",""], key=None, base_url=None):
|
|
47
|
-
self.org, self.api_token = org, api_token
|
|
48
|
-
self.theme = resolve_theme(theme)
|
|
49
|
-
self.key, self.modal_keys, self.pip, self.apt, self.copy, self.copy_public = key, modal_keys, pip, apt, copy, copy_public
|
|
50
|
-
self.base_url = base_url
|
|
51
|
-
|
|
52
|
-
self.registered_functions = []
|
|
53
|
-
|
|
54
|
-
def __call__(self, name=None, header="", intro="", title="", domain=None, auth=False, tier="free", analytics=False):
|
|
55
|
-
if tier=="cycls_pass":
|
|
56
|
-
auth=True
|
|
57
|
-
analytics=True
|
|
58
|
-
def decorator(f):
|
|
59
|
-
agent_name = name or f.__name__.replace('_', '-')
|
|
60
|
-
self.registered_functions.append(RegisteredAgent(
|
|
61
|
-
func=f,
|
|
62
|
-
name=agent_name,
|
|
63
|
-
domain=domain or f"{agent_name}.cycls.ai",
|
|
64
|
-
config=Config(
|
|
65
|
-
header=header,
|
|
66
|
-
intro=intro,
|
|
67
|
-
title=title,
|
|
68
|
-
auth=auth,
|
|
69
|
-
tier=tier,
|
|
70
|
-
analytics=analytics,
|
|
71
|
-
org=self.org,
|
|
72
|
-
),
|
|
73
|
-
))
|
|
74
|
-
return f
|
|
75
|
-
return decorator
|
|
76
|
-
|
|
77
|
-
def _local(self, port=8080, watch=True):
|
|
78
|
-
"""Run directly with uvicorn (no Docker)."""
|
|
79
|
-
if not self.registered_functions:
|
|
80
|
-
print("Error: No @agent decorated function found.")
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
agent = self.registered_functions[0]
|
|
84
|
-
if len(self.registered_functions) > 1:
|
|
85
|
-
print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
|
|
86
|
-
print(f"🚀 Starting local server at localhost:{port}")
|
|
87
|
-
agent.config.public_path = self.theme
|
|
88
|
-
set_prod(agent.config, False)
|
|
89
|
-
uvicorn.run(web(agent.func, agent.config), host="0.0.0.0", port=port, reload=watch)
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
def _runtime(self, prod=False):
|
|
93
|
-
"""Create a Runtime instance for the first registered agent."""
|
|
94
|
-
if not self.registered_functions:
|
|
95
|
-
print("Error: No @agent decorated function found.")
|
|
96
|
-
return None
|
|
97
|
-
|
|
98
|
-
agent = self.registered_functions[0]
|
|
99
|
-
if len(self.registered_functions) > 1:
|
|
100
|
-
print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
|
|
101
|
-
|
|
102
|
-
set_prod(agent.config, prod)
|
|
103
|
-
func = agent.func
|
|
104
|
-
name = agent.name
|
|
105
|
-
config_dict = agent.config.model_dump()
|
|
106
|
-
|
|
107
|
-
files = {str(self.theme): "theme", str(CYCLS_PATH)+"/web.py": "web.py"}
|
|
108
|
-
files.update({f: f for f in self.copy})
|
|
109
|
-
files.update({f: f"public/{f}" for f in self.copy_public})
|
|
110
|
-
|
|
111
|
-
return Runtime(
|
|
112
|
-
func=lambda port: __import__("web").serve(func, config_dict, name, port),
|
|
113
|
-
name=name,
|
|
114
|
-
apt_packages=self.apt,
|
|
115
|
-
pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
|
|
116
|
-
copy=files,
|
|
117
|
-
base_url=self.base_url,
|
|
118
|
-
api_key=self.key
|
|
119
|
-
)
|
|
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)
|
|
138
|
-
|
|
139
|
-
def modal(self, prod=False):
|
|
140
|
-
import modal
|
|
141
|
-
from modal.runner import run_app
|
|
142
|
-
self.client = modal.Client.from_credentials(*self.modal_keys)
|
|
143
|
-
image = (modal.Image.debian_slim()
|
|
144
|
-
.pip_install("fastapi[standard]", "pyjwt", "cryptography", *self.pip)
|
|
145
|
-
.apt_install(*self.apt)
|
|
146
|
-
.add_local_dir(self.theme, "/root/theme")
|
|
147
|
-
.add_local_file(str(CYCLS_PATH)+"/web.py", "/root/web.py"))
|
|
148
|
-
|
|
149
|
-
for item in self.copy:
|
|
150
|
-
image = image.add_local_file(item, f"/root/{item}") if "." in item else image.add_local_dir(item, f'/root/{item}')
|
|
151
|
-
|
|
152
|
-
for item in self.copy_public:
|
|
153
|
-
image = image.add_local_file(item, f"/root/public/{item}") if "." in item else image.add_local_dir(item, f'/root/public/{item}')
|
|
154
|
-
|
|
155
|
-
self.app = modal.App("development", image=image)
|
|
156
|
-
|
|
157
|
-
if not self.registered_functions:
|
|
158
|
-
print("Error: No @agent decorated function found.")
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
for agent in self.registered_functions:
|
|
162
|
-
set_prod(agent.config, prod)
|
|
163
|
-
func = agent.func
|
|
164
|
-
name = agent.name
|
|
165
|
-
domain = agent.domain
|
|
166
|
-
config_dict = agent.config.model_dump()
|
|
167
|
-
self.app.function(serialized=True, name=name)(
|
|
168
|
-
modal.asgi_app(label=name, custom_domains=[domain])
|
|
169
|
-
(lambda: __import__("web").web(func, config_dict))
|
|
170
|
-
)
|
|
171
|
-
if prod:
|
|
172
|
-
for agent in self.registered_functions:
|
|
173
|
-
print(f"✅ Deployed to ⇒ https://{agent.domain}")
|
|
174
|
-
self.app.deploy(client=self.client, name=self.registered_functions[0].name)
|
|
175
|
-
return
|
|
176
|
-
else:
|
|
177
|
-
with modal.enable_output():
|
|
178
|
-
run_app(app=self.app, client=self.client)
|
|
179
|
-
print(" Modal development server is running. Press Ctrl+C to stop.")
|
|
180
|
-
with modal.enable_output(), run_app(app=self.app, client=self.client):
|
|
181
|
-
while True: time.sleep(10)
|
|
182
|
-
|
|
183
|
-
# docker system prune -af
|
|
184
|
-
# poetry config pypi-token.pypi <your-token>
|
|
185
|
-
# poetry run python cake.py
|
|
186
|
-
# poetry publish --build
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|