cycls 0.0.1.1__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.86/PKG-INFO +276 -0
- cycls-0.0.2.86/README.md +254 -0
- cycls-0.0.2.86/cycls/__init__.py +14 -0
- cycls-0.0.2.86/cycls/app.py +88 -0
- cycls-0.0.2.86/cycls/auth.py +4 -0
- cycls-0.0.2.86/cycls/cli.py +104 -0
- cycls-0.0.2.86/cycls/function.py +404 -0
- cycls-0.0.2.86/cycls/themes/default/assets/index-C2r4Daz3.js +435 -0
- cycls-0.0.2.86/cycls/themes/default/assets/index-DWGS8zpa.css +1 -0
- cycls-0.0.2.86/cycls/themes/default/index.html +28 -0
- cycls-0.0.2.86/cycls/themes/dev/index.html +298 -0
- cycls-0.0.2.86/cycls/web.py +149 -0
- cycls-0.0.2.86/pyproject.toml +28 -0
- cycls-0.0.1.1/LICENSE +0 -19
- cycls-0.0.1.1/PKG-INFO +0 -17
- cycls-0.0.1.1/cycls/UI.py +0 -26
- cycls-0.0.1.1/cycls/__init__.py +0 -3
- cycls-0.0.1.1/cycls/client.py +0 -160
- cycls-0.0.1.1/cycls/configuration.py +0 -27
- cycls-0.0.1.1/cycls/static.py +0 -3
- cycls-0.0.1.1/cycls/typings.py +0 -119
- cycls-0.0.1.1/pyproject.toml +0 -25
cycls-0.0.2.86/PKG-INFO
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cycls
|
|
3
|
+
Version: 0.0.2.86
|
|
4
|
+
Summary: Distribute Intelligence
|
|
5
|
+
Author: Mohammed J. AlRujayi
|
|
6
|
+
Author-email: mj@cycls.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Dist: cloudpickle (>=3.1.1,<4.0.0)
|
|
16
|
+
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
|
17
|
+
Requires-Dist: fastapi (>=0.111.0,<0.112.0)
|
|
18
|
+
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
|
19
|
+
Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<h3 align="center">
|
|
23
|
+
Distribute Intelligence
|
|
24
|
+
</h3>
|
|
25
|
+
|
|
26
|
+
<h4 align="center">
|
|
27
|
+
<a href="https://cycls.com">Website</a> |
|
|
28
|
+
<a href="https://docs.cycls.com">Docs</a>
|
|
29
|
+
</h4>
|
|
30
|
+
|
|
31
|
+
<h4 align="center">
|
|
32
|
+
<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>
|
|
33
|
+
<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>
|
|
34
|
+
<a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
|
|
35
|
+
<a href="https://x.com/cyclsai">
|
|
36
|
+
<img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
|
|
37
|
+
</a>
|
|
38
|
+
</h4>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# Cycls
|
|
43
|
+
|
|
44
|
+
The open-source SDK for distributing AI agents.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Agent extends App (prompts, skills)
|
|
48
|
+
└── App extends Function (web UI)
|
|
49
|
+
└── Function (containerization)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Distribute Intelligence
|
|
53
|
+
|
|
54
|
+
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import cycls
|
|
58
|
+
|
|
59
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
60
|
+
|
|
61
|
+
@cycls.app(pip=["openai"])
|
|
62
|
+
async def app(context):
|
|
63
|
+
from openai import AsyncOpenAI
|
|
64
|
+
client = AsyncOpenAI()
|
|
65
|
+
|
|
66
|
+
stream = await client.responses.create(
|
|
67
|
+
model="o3-mini",
|
|
68
|
+
input=context.messages,
|
|
69
|
+
stream=True,
|
|
70
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async for event in stream:
|
|
74
|
+
if event.type == "response.reasoning_summary_text.delta":
|
|
75
|
+
yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
|
|
76
|
+
elif event.type == "response.output_text.delta":
|
|
77
|
+
yield event.delta
|
|
78
|
+
|
|
79
|
+
app.deploy() # Live at https://agent.cycls.ai
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install cycls
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Requires Docker.
|
|
89
|
+
|
|
90
|
+
## What You Get
|
|
91
|
+
|
|
92
|
+
- **Streaming API** - OpenAI-compatible `/chat/completions` endpoint
|
|
93
|
+
- **Web Interface** - Chat UI served automatically
|
|
94
|
+
- **Authentication** - `auth=True` enables JWT-based access control
|
|
95
|
+
- **Analytics** - `analytics=True` tracks usage
|
|
96
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
97
|
+
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
98
|
+
|
|
99
|
+
## Running
|
|
100
|
+
|
|
101
|
+
```python
|
|
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
|
+
```
|
|
106
|
+
|
|
107
|
+
Get an API key at [cycls.com](https://cycls.com).
|
|
108
|
+
|
|
109
|
+
## Authentication & Analytics
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
@cycls.app(pip=["openai"], auth=True, analytics=True)
|
|
113
|
+
async def app(context):
|
|
114
|
+
# context.user available when auth=True
|
|
115
|
+
user = context.user # User(id, email, name, plans)
|
|
116
|
+
yield f"Hello {user.name}!"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Flag | Description |
|
|
120
|
+
|------|-------------|
|
|
121
|
+
| `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
|
|
122
|
+
| `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
|
|
123
|
+
| `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
|
|
124
|
+
|
|
125
|
+
## Native UI Components
|
|
126
|
+
|
|
127
|
+
Yield structured objects for rich streaming responses:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
@cycls.app()
|
|
131
|
+
async def demo(context):
|
|
132
|
+
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
133
|
+
yield "Here's what I found:\n\n"
|
|
134
|
+
|
|
135
|
+
yield {"type": "table", "headers": ["Name", "Status"]}
|
|
136
|
+
yield {"type": "table", "row": ["Server 1", "Online"]}
|
|
137
|
+
yield {"type": "table", "row": ["Server 2", "Offline"]}
|
|
138
|
+
|
|
139
|
+
yield {"type": "code", "code": "result = analyze(data)", "language": "python"}
|
|
140
|
+
yield {"type": "callout", "callout": "Analysis complete!", "style": "success"}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Component | Streaming |
|
|
144
|
+
|-----------|-----------|
|
|
145
|
+
| `{"type": "thinking", "thinking": "..."}` | Yes |
|
|
146
|
+
| `{"type": "code", "code": "...", "language": "..."}` | Yes |
|
|
147
|
+
| `{"type": "table", "headers": [...]}` | Yes |
|
|
148
|
+
| `{"type": "table", "row": [...]}` | Yes |
|
|
149
|
+
| `{"type": "status", "status": "..."}` | Yes |
|
|
150
|
+
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
151
|
+
| `{"type": "image", "src": "..."}` | Yes |
|
|
152
|
+
|
|
153
|
+
### Thinking Bubbles
|
|
154
|
+
|
|
155
|
+
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:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Multiple yields build one thinking bubble
|
|
159
|
+
yield {"type": "thinking", "thinking": "Let me "}
|
|
160
|
+
yield {"type": "thinking", "thinking": "analyze this..."}
|
|
161
|
+
yield {"type": "thinking", "thinking": " Done thinking."}
|
|
162
|
+
|
|
163
|
+
# Then output the response
|
|
164
|
+
yield "Here's what I found..."
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
168
|
+
|
|
169
|
+
## Context Object
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
@cycls.app()
|
|
173
|
+
async def chat(context):
|
|
174
|
+
context.messages # [{"role": "user", "content": "..."}]
|
|
175
|
+
context.messages.raw # Full data including UI component parts
|
|
176
|
+
context.user # User(id, email, name, plans) when auth=True
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## API Endpoints
|
|
180
|
+
|
|
181
|
+
| Endpoint | Format |
|
|
182
|
+
|----------|--------|
|
|
183
|
+
| `POST chat/cycls` | Cycls streaming protocol |
|
|
184
|
+
| `POST chat/completions` | OpenAI-compatible |
|
|
185
|
+
|
|
186
|
+
## Streaming Protocol
|
|
187
|
+
|
|
188
|
+
Cycls streams structured components over SSE:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
data: {"type": "thinking", "thinking": "Let me "}
|
|
192
|
+
data: {"type": "thinking", "thinking": "analyze..."}
|
|
193
|
+
data: {"type": "text", "text": "Here's the answer"}
|
|
194
|
+
data: {"type": "callout", "callout": "Done!", "style": "success"}
|
|
195
|
+
data: [DONE]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integration.
|
|
199
|
+
|
|
200
|
+
## Declarative Infrastructure
|
|
201
|
+
|
|
202
|
+
Define your entire runtime in the decorator:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
@cycls.app(
|
|
206
|
+
pip=["openai", "pandas", "numpy"],
|
|
207
|
+
apt=["ffmpeg", "libmagic1"],
|
|
208
|
+
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
209
|
+
copy_public=["./assets/logo.png", "./static/"],
|
|
210
|
+
)
|
|
211
|
+
async def my_app(context):
|
|
212
|
+
...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `pip` - Python Packages
|
|
216
|
+
|
|
217
|
+
Install any packages from PyPI. These are installed during the container build.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
pip=["openai", "pandas", "numpy", "transformers"]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `apt` - System Packages
|
|
224
|
+
|
|
225
|
+
Install system-level dependencies via apt-get. Need ffmpeg for audio processing? ImageMagick for images? Just declare it.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `copy` - Bundle Files and Directories
|
|
232
|
+
|
|
233
|
+
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
copy=[
|
|
237
|
+
"./utils.py", # Single file, relative path
|
|
238
|
+
"./models/", # Entire directory
|
|
239
|
+
"/home/user/configs/app.json", # Absolute path
|
|
240
|
+
]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Then import them in your function:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
@cycls.app(copy=["./utils.py"])
|
|
247
|
+
async def chat(context):
|
|
248
|
+
from utils import helper_function # Your bundled module
|
|
249
|
+
...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `copy_public` - Static Files
|
|
253
|
+
|
|
254
|
+
Files and directories served at the `/public` endpoint. Perfect for images, downloads, or any static assets your agent needs to reference.
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
copy_public=["./assets/logo.png", "./downloads/"]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Access them at `https://your-app.cycls.ai/public/logo.png`.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### What You Get
|
|
265
|
+
|
|
266
|
+
- **One file** - Code, dependencies, configuration, and infrastructure together
|
|
267
|
+
- **Instant deploys** - Unchanged code deploys in seconds from cache
|
|
268
|
+
- **No drift** - What you see is what runs. Always.
|
|
269
|
+
- **Just works** - Closures, lambdas, dynamic imports - your function runs exactly as written
|
|
270
|
+
|
|
271
|
+
No YAML. No Dockerfiles. No infrastructure repo. The code is the deployment.
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT
|
|
276
|
+
|
cycls-0.0.2.86/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<h3 align="center">
|
|
2
|
+
Distribute Intelligence
|
|
3
|
+
</h3>
|
|
4
|
+
|
|
5
|
+
<h4 align="center">
|
|
6
|
+
<a href="https://cycls.com">Website</a> |
|
|
7
|
+
<a href="https://docs.cycls.com">Docs</a>
|
|
8
|
+
</h4>
|
|
9
|
+
|
|
10
|
+
<h4 align="center">
|
|
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>
|
|
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>
|
|
14
|
+
<a href="https://x.com/cyclsai">
|
|
15
|
+
<img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
|
|
16
|
+
</a>
|
|
17
|
+
</h4>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Cycls
|
|
22
|
+
|
|
23
|
+
The open-source SDK for distributing AI agents.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Agent extends App (prompts, skills)
|
|
27
|
+
└── App extends Function (web UI)
|
|
28
|
+
└── Function (containerization)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Distribute Intelligence
|
|
32
|
+
|
|
33
|
+
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import cycls
|
|
37
|
+
|
|
38
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
39
|
+
|
|
40
|
+
@cycls.app(pip=["openai"])
|
|
41
|
+
async def app(context):
|
|
42
|
+
from openai import AsyncOpenAI
|
|
43
|
+
client = AsyncOpenAI()
|
|
44
|
+
|
|
45
|
+
stream = await client.responses.create(
|
|
46
|
+
model="o3-mini",
|
|
47
|
+
input=context.messages,
|
|
48
|
+
stream=True,
|
|
49
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async for event in stream:
|
|
53
|
+
if event.type == "response.reasoning_summary_text.delta":
|
|
54
|
+
yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
|
|
55
|
+
elif event.type == "response.output_text.delta":
|
|
56
|
+
yield event.delta
|
|
57
|
+
|
|
58
|
+
app.deploy() # Live at https://agent.cycls.ai
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install cycls
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Docker.
|
|
68
|
+
|
|
69
|
+
## What You Get
|
|
70
|
+
|
|
71
|
+
- **Streaming API** - OpenAI-compatible `/chat/completions` endpoint
|
|
72
|
+
- **Web Interface** - Chat UI served automatically
|
|
73
|
+
- **Authentication** - `auth=True` enables JWT-based access control
|
|
74
|
+
- **Analytics** - `analytics=True` tracks usage
|
|
75
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
76
|
+
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
77
|
+
|
|
78
|
+
## Running
|
|
79
|
+
|
|
80
|
+
```python
|
|
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
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Get an API key at [cycls.com](https://cycls.com).
|
|
87
|
+
|
|
88
|
+
## Authentication & Analytics
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
@cycls.app(pip=["openai"], auth=True, analytics=True)
|
|
92
|
+
async def app(context):
|
|
93
|
+
# context.user available when auth=True
|
|
94
|
+
user = context.user # User(id, email, name, plans)
|
|
95
|
+
yield f"Hello {user.name}!"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
|
|
101
|
+
| `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
|
|
102
|
+
| `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
|
|
103
|
+
|
|
104
|
+
## Native UI Components
|
|
105
|
+
|
|
106
|
+
Yield structured objects for rich streaming responses:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
@cycls.app()
|
|
110
|
+
async def demo(context):
|
|
111
|
+
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
112
|
+
yield "Here's what I found:\n\n"
|
|
113
|
+
|
|
114
|
+
yield {"type": "table", "headers": ["Name", "Status"]}
|
|
115
|
+
yield {"type": "table", "row": ["Server 1", "Online"]}
|
|
116
|
+
yield {"type": "table", "row": ["Server 2", "Offline"]}
|
|
117
|
+
|
|
118
|
+
yield {"type": "code", "code": "result = analyze(data)", "language": "python"}
|
|
119
|
+
yield {"type": "callout", "callout": "Analysis complete!", "style": "success"}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Component | Streaming |
|
|
123
|
+
|-----------|-----------|
|
|
124
|
+
| `{"type": "thinking", "thinking": "..."}` | Yes |
|
|
125
|
+
| `{"type": "code", "code": "...", "language": "..."}` | Yes |
|
|
126
|
+
| `{"type": "table", "headers": [...]}` | Yes |
|
|
127
|
+
| `{"type": "table", "row": [...]}` | Yes |
|
|
128
|
+
| `{"type": "status", "status": "..."}` | Yes |
|
|
129
|
+
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
130
|
+
| `{"type": "image", "src": "..."}` | Yes |
|
|
131
|
+
|
|
132
|
+
### Thinking Bubbles
|
|
133
|
+
|
|
134
|
+
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:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Multiple yields build one thinking bubble
|
|
138
|
+
yield {"type": "thinking", "thinking": "Let me "}
|
|
139
|
+
yield {"type": "thinking", "thinking": "analyze this..."}
|
|
140
|
+
yield {"type": "thinking", "thinking": " Done thinking."}
|
|
141
|
+
|
|
142
|
+
# Then output the response
|
|
143
|
+
yield "Here's what I found..."
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
147
|
+
|
|
148
|
+
## Context Object
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
@cycls.app()
|
|
152
|
+
async def chat(context):
|
|
153
|
+
context.messages # [{"role": "user", "content": "..."}]
|
|
154
|
+
context.messages.raw # Full data including UI component parts
|
|
155
|
+
context.user # User(id, email, name, plans) when auth=True
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## API Endpoints
|
|
159
|
+
|
|
160
|
+
| Endpoint | Format |
|
|
161
|
+
|----------|--------|
|
|
162
|
+
| `POST chat/cycls` | Cycls streaming protocol |
|
|
163
|
+
| `POST chat/completions` | OpenAI-compatible |
|
|
164
|
+
|
|
165
|
+
## Streaming Protocol
|
|
166
|
+
|
|
167
|
+
Cycls streams structured components over SSE:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
data: {"type": "thinking", "thinking": "Let me "}
|
|
171
|
+
data: {"type": "thinking", "thinking": "analyze..."}
|
|
172
|
+
data: {"type": "text", "text": "Here's the answer"}
|
|
173
|
+
data: {"type": "callout", "callout": "Done!", "style": "success"}
|
|
174
|
+
data: [DONE]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integration.
|
|
178
|
+
|
|
179
|
+
## Declarative Infrastructure
|
|
180
|
+
|
|
181
|
+
Define your entire runtime in the decorator:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
@cycls.app(
|
|
185
|
+
pip=["openai", "pandas", "numpy"],
|
|
186
|
+
apt=["ffmpeg", "libmagic1"],
|
|
187
|
+
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
188
|
+
copy_public=["./assets/logo.png", "./static/"],
|
|
189
|
+
)
|
|
190
|
+
async def my_app(context):
|
|
191
|
+
...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `pip` - Python Packages
|
|
195
|
+
|
|
196
|
+
Install any packages from PyPI. These are installed during the container build.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
pip=["openai", "pandas", "numpy", "transformers"]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `apt` - System Packages
|
|
203
|
+
|
|
204
|
+
Install system-level dependencies via apt-get. Need ffmpeg for audio processing? ImageMagick for images? Just declare it.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `copy` - Bundle Files and Directories
|
|
211
|
+
|
|
212
|
+
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
copy=[
|
|
216
|
+
"./utils.py", # Single file, relative path
|
|
217
|
+
"./models/", # Entire directory
|
|
218
|
+
"/home/user/configs/app.json", # Absolute path
|
|
219
|
+
]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Then import them in your function:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
@cycls.app(copy=["./utils.py"])
|
|
226
|
+
async def chat(context):
|
|
227
|
+
from utils import helper_function # Your bundled module
|
|
228
|
+
...
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `copy_public` - Static Files
|
|
232
|
+
|
|
233
|
+
Files and directories served at the `/public` endpoint. Perfect for images, downloads, or any static assets your agent needs to reference.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
copy_public=["./assets/logo.png", "./downloads/"]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Access them at `https://your-app.cycls.ai/public/logo.png`.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### What You Get
|
|
244
|
+
|
|
245
|
+
- **One file** - Code, dependencies, configuration, and infrastructure together
|
|
246
|
+
- **Instant deploys** - Unchanged code deploys in seconds from cache
|
|
247
|
+
- **No drift** - What you see is what runs. Always.
|
|
248
|
+
- **Just works** - Closures, lambdas, dynamic imports - your function runs exactly as written
|
|
249
|
+
|
|
250
|
+
No YAML. No Dockerfiles. No infrastructure repo. The code is the deployment.
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
|
@@ -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
|