cycls 0.0.2.72__tar.gz → 0.0.2.73__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.73}/PKG-INFO +52 -48
- {cycls-0.0.2.72 → cycls-0.0.2.73}/README.md +51 -47
- cycls-0.0.2.73/cycls/__init__.py +15 -0
- cycls-0.0.2.73/cycls/sdk.py +181 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/cycls/web.py +1 -1
- {cycls-0.0.2.72 → cycls-0.0.2.73}/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.73}/cycls/auth.py +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/cycls/cli.py +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/cycls/default-theme/assets/index-B0ZKcm_V.css +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/cycls/default-theme/assets/index-D5EDcI4J.js +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/cycls/default-theme/index.html +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/cycls/dev-theme/index.html +0 -0
- {cycls-0.0.2.72 → cycls-0.0.2.73}/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.73
|
|
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" />
|
|
@@ -53,24 +54,27 @@ Write a function. Deploy it as an API, a web interface, or both. Add authenticat
|
|
|
53
54
|
```python
|
|
54
55
|
import cycls
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
57
58
|
|
|
58
|
-
@agent("
|
|
59
|
-
async def
|
|
59
|
+
@cycls.agent(pip=["openai"])
|
|
60
|
+
async def agent(context):
|
|
60
61
|
from openai import AsyncOpenAI
|
|
61
62
|
client = AsyncOpenAI()
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
model="
|
|
65
|
-
|
|
66
|
-
stream=True
|
|
64
|
+
stream = await client.responses.create(
|
|
65
|
+
model="o3-mini",
|
|
66
|
+
input=context.messages,
|
|
67
|
+
stream=True,
|
|
68
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
67
69
|
)
|
|
68
70
|
|
|
69
|
-
async for
|
|
70
|
-
if
|
|
71
|
-
yield
|
|
71
|
+
async for event in stream:
|
|
72
|
+
if event.type == "response.reasoning_summary_text.delta":
|
|
73
|
+
yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
|
|
74
|
+
elif event.type == "response.output_text.delta":
|
|
75
|
+
yield event.delta
|
|
72
76
|
|
|
73
|
-
agent.deploy() # Live at https://
|
|
77
|
+
agent.deploy() # Live at https://agent.cycls.ai
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
## Installation
|
|
@@ -87,7 +91,7 @@ Requires Docker.
|
|
|
87
91
|
- **Web Interface** - Chat UI served automatically
|
|
88
92
|
- **Authentication** - `auth=True` enables JWT-based access control
|
|
89
93
|
- **Analytics** - `analytics=True` tracks usage
|
|
90
|
-
- **Monetization** - `
|
|
94
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
91
95
|
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
92
96
|
|
|
93
97
|
## Running
|
|
@@ -95,17 +99,33 @@ Requires Docker.
|
|
|
95
99
|
```python
|
|
96
100
|
agent.local() # Development with hot-reload (localhost:8080)
|
|
97
101
|
agent.local(watch=False) # Development without hot-reload
|
|
98
|
-
agent.deploy() # Production: https://agent
|
|
102
|
+
agent.deploy() # Production: https://agent.cycls.ai
|
|
99
103
|
```
|
|
100
104
|
|
|
101
105
|
Get an API key at [cycls.com](https://cycls.com).
|
|
102
106
|
|
|
107
|
+
## Authentication & Analytics
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
@cycls.agent(pip=["openai"], auth=True, analytics=True)
|
|
111
|
+
async def agent(context):
|
|
112
|
+
# context.user available when auth=True
|
|
113
|
+
user = context.user # User(id, email, name, plans)
|
|
114
|
+
yield f"Hello {user.name}!"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Flag | Description |
|
|
118
|
+
|------|-------------|
|
|
119
|
+
| `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
|
|
120
|
+
| `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
|
|
121
|
+
| `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
|
|
122
|
+
|
|
103
123
|
## Native UI Components
|
|
104
124
|
|
|
105
125
|
Yield structured objects for rich streaming responses:
|
|
106
126
|
|
|
107
127
|
```python
|
|
108
|
-
@agent()
|
|
128
|
+
@cycls.agent()
|
|
109
129
|
async def demo(context):
|
|
110
130
|
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
111
131
|
yield "Here's what I found:\n\n"
|
|
@@ -128,32 +148,26 @@ async def demo(context):
|
|
|
128
148
|
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
129
149
|
| `{"type": "image", "src": "..."}` | Yes |
|
|
130
150
|
|
|
131
|
-
###
|
|
151
|
+
### Thinking Bubbles
|
|
132
152
|
|
|
133
|
-
|
|
134
|
-
@agent()
|
|
135
|
-
async def chat(context):
|
|
136
|
-
from openai import AsyncOpenAI
|
|
137
|
-
client = AsyncOpenAI()
|
|
153
|
+
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
154
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
155
|
+
```python
|
|
156
|
+
# Multiple yields build one thinking bubble
|
|
157
|
+
yield {"type": "thinking", "thinking": "Let me "}
|
|
158
|
+
yield {"type": "thinking", "thinking": "analyze this..."}
|
|
159
|
+
yield {"type": "thinking", "thinking": " Done thinking."}
|
|
145
160
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
yield {"type": "thinking", "thinking": event.delta}
|
|
149
|
-
elif event.type == "response.output_text.delta":
|
|
150
|
-
yield event.delta
|
|
161
|
+
# Then output the response
|
|
162
|
+
yield "Here's what I found..."
|
|
151
163
|
```
|
|
152
164
|
|
|
165
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
166
|
+
|
|
153
167
|
## Context Object
|
|
154
168
|
|
|
155
169
|
```python
|
|
156
|
-
@agent()
|
|
170
|
+
@cycls.agent()
|
|
157
171
|
async def chat(context):
|
|
158
172
|
context.messages # [{"role": "user", "content": "..."}]
|
|
159
173
|
context.messages.raw # Full data including UI component parts
|
|
@@ -183,16 +197,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
|
|
|
183
197
|
|
|
184
198
|
## Declarative Infrastructure
|
|
185
199
|
|
|
186
|
-
Define your entire runtime in
|
|
200
|
+
Define your entire runtime in the decorator:
|
|
187
201
|
|
|
188
202
|
```python
|
|
189
|
-
|
|
203
|
+
@cycls.agent(
|
|
190
204
|
pip=["openai", "pandas", "numpy"],
|
|
191
205
|
apt=["ffmpeg", "libmagic1"],
|
|
192
|
-
run_commands=["curl -sSL https://example.com/setup.sh | bash"],
|
|
193
206
|
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
194
207
|
copy_public=["./assets/logo.png", "./static/"],
|
|
195
208
|
)
|
|
209
|
+
async def my_agent(context):
|
|
210
|
+
...
|
|
196
211
|
```
|
|
197
212
|
|
|
198
213
|
### `pip` - Python Packages
|
|
@@ -211,17 +226,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
|
|
|
211
226
|
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
212
227
|
```
|
|
213
228
|
|
|
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
229
|
### `copy` - Bundle Files and Directories
|
|
226
230
|
|
|
227
231
|
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
@@ -237,7 +241,7 @@ copy=[
|
|
|
237
241
|
Then import them in your function:
|
|
238
242
|
|
|
239
243
|
```python
|
|
240
|
-
@agent()
|
|
244
|
+
@cycls.agent(copy=["./utils.py"])
|
|
241
245
|
async def chat(context):
|
|
242
246
|
from utils import helper_function # Your bundled module
|
|
243
247
|
...
|
|
@@ -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" />
|
|
@@ -30,24 +31,27 @@ Write a function. Deploy it as an API, a web interface, or both. Add authenticat
|
|
|
30
31
|
```python
|
|
31
32
|
import cycls
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
34
35
|
|
|
35
|
-
@agent("
|
|
36
|
-
async def
|
|
36
|
+
@cycls.agent(pip=["openai"])
|
|
37
|
+
async def agent(context):
|
|
37
38
|
from openai import AsyncOpenAI
|
|
38
39
|
client = AsyncOpenAI()
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
model="
|
|
42
|
-
|
|
43
|
-
stream=True
|
|
41
|
+
stream = await client.responses.create(
|
|
42
|
+
model="o3-mini",
|
|
43
|
+
input=context.messages,
|
|
44
|
+
stream=True,
|
|
45
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
44
46
|
)
|
|
45
47
|
|
|
46
|
-
async for
|
|
47
|
-
if
|
|
48
|
-
yield
|
|
48
|
+
async for event in stream:
|
|
49
|
+
if event.type == "response.reasoning_summary_text.delta":
|
|
50
|
+
yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
|
|
51
|
+
elif event.type == "response.output_text.delta":
|
|
52
|
+
yield event.delta
|
|
49
53
|
|
|
50
|
-
agent.deploy() # Live at https://
|
|
54
|
+
agent.deploy() # Live at https://agent.cycls.ai
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
## Installation
|
|
@@ -64,7 +68,7 @@ Requires Docker.
|
|
|
64
68
|
- **Web Interface** - Chat UI served automatically
|
|
65
69
|
- **Authentication** - `auth=True` enables JWT-based access control
|
|
66
70
|
- **Analytics** - `analytics=True` tracks usage
|
|
67
|
-
- **Monetization** - `
|
|
71
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
68
72
|
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
69
73
|
|
|
70
74
|
## Running
|
|
@@ -72,17 +76,33 @@ Requires Docker.
|
|
|
72
76
|
```python
|
|
73
77
|
agent.local() # Development with hot-reload (localhost:8080)
|
|
74
78
|
agent.local(watch=False) # Development without hot-reload
|
|
75
|
-
agent.deploy() # Production: https://agent
|
|
79
|
+
agent.deploy() # Production: https://agent.cycls.ai
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
Get an API key at [cycls.com](https://cycls.com).
|
|
79
83
|
|
|
84
|
+
## Authentication & Analytics
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
@cycls.agent(pip=["openai"], auth=True, analytics=True)
|
|
88
|
+
async def agent(context):
|
|
89
|
+
# context.user available when auth=True
|
|
90
|
+
user = context.user # User(id, email, name, plans)
|
|
91
|
+
yield f"Hello {user.name}!"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Flag | Description |
|
|
95
|
+
|------|-------------|
|
|
96
|
+
| `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
|
|
97
|
+
| `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
|
|
98
|
+
| `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
|
|
99
|
+
|
|
80
100
|
## Native UI Components
|
|
81
101
|
|
|
82
102
|
Yield structured objects for rich streaming responses:
|
|
83
103
|
|
|
84
104
|
```python
|
|
85
|
-
@agent()
|
|
105
|
+
@cycls.agent()
|
|
86
106
|
async def demo(context):
|
|
87
107
|
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
88
108
|
yield "Here's what I found:\n\n"
|
|
@@ -105,32 +125,26 @@ async def demo(context):
|
|
|
105
125
|
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
106
126
|
| `{"type": "image", "src": "..."}` | Yes |
|
|
107
127
|
|
|
108
|
-
###
|
|
128
|
+
### Thinking Bubbles
|
|
109
129
|
|
|
110
|
-
|
|
111
|
-
@agent()
|
|
112
|
-
async def chat(context):
|
|
113
|
-
from openai import AsyncOpenAI
|
|
114
|
-
client = AsyncOpenAI()
|
|
130
|
+
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
131
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
132
|
+
```python
|
|
133
|
+
# Multiple yields build one thinking bubble
|
|
134
|
+
yield {"type": "thinking", "thinking": "Let me "}
|
|
135
|
+
yield {"type": "thinking", "thinking": "analyze this..."}
|
|
136
|
+
yield {"type": "thinking", "thinking": " Done thinking."}
|
|
122
137
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
yield {"type": "thinking", "thinking": event.delta}
|
|
126
|
-
elif event.type == "response.output_text.delta":
|
|
127
|
-
yield event.delta
|
|
138
|
+
# Then output the response
|
|
139
|
+
yield "Here's what I found..."
|
|
128
140
|
```
|
|
129
141
|
|
|
142
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
143
|
+
|
|
130
144
|
## Context Object
|
|
131
145
|
|
|
132
146
|
```python
|
|
133
|
-
@agent()
|
|
147
|
+
@cycls.agent()
|
|
134
148
|
async def chat(context):
|
|
135
149
|
context.messages # [{"role": "user", "content": "..."}]
|
|
136
150
|
context.messages.raw # Full data including UI component parts
|
|
@@ -160,16 +174,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
|
|
|
160
174
|
|
|
161
175
|
## Declarative Infrastructure
|
|
162
176
|
|
|
163
|
-
Define your entire runtime in
|
|
177
|
+
Define your entire runtime in the decorator:
|
|
164
178
|
|
|
165
179
|
```python
|
|
166
|
-
|
|
180
|
+
@cycls.agent(
|
|
167
181
|
pip=["openai", "pandas", "numpy"],
|
|
168
182
|
apt=["ffmpeg", "libmagic1"],
|
|
169
|
-
run_commands=["curl -sSL https://example.com/setup.sh | bash"],
|
|
170
183
|
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
171
184
|
copy_public=["./assets/logo.png", "./static/"],
|
|
172
185
|
)
|
|
186
|
+
async def my_agent(context):
|
|
187
|
+
...
|
|
173
188
|
```
|
|
174
189
|
|
|
175
190
|
### `pip` - Python Packages
|
|
@@ -188,17 +203,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
|
|
|
188
203
|
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
189
204
|
```
|
|
190
205
|
|
|
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
206
|
### `copy` - Bundle Files and Directories
|
|
203
207
|
|
|
204
208
|
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
@@ -214,7 +218,7 @@ copy=[
|
|
|
214
218
|
Then import them in your function:
|
|
215
219
|
|
|
216
220
|
```python
|
|
217
|
-
@agent()
|
|
221
|
+
@cycls.agent(copy=["./utils.py"])
|
|
218
222
|
async def chat(context):
|
|
219
223
|
from utils import helper_function # Your bundled module
|
|
220
224
|
...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .sdk import function, agent
|
|
2
|
+
from .runtime import Runtime
|
|
3
|
+
|
|
4
|
+
def __getattr__(name):
|
|
5
|
+
from . import sdk
|
|
6
|
+
if name in ("api_key", "base_url"):
|
|
7
|
+
return getattr(sdk, name)
|
|
8
|
+
raise AttributeError(f"module 'cycls' has no attribute '{name}'")
|
|
9
|
+
|
|
10
|
+
def __setattr__(name, value):
|
|
11
|
+
from . import sdk
|
|
12
|
+
if name in ("api_key", "base_url"):
|
|
13
|
+
setattr(sdk, name, value)
|
|
14
|
+
return
|
|
15
|
+
raise AttributeError(f"module 'cycls' has no attribute '{name}'")
|
|
@@ -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.73"
|
|
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
|