cycls 0.0.1.1__tar.gz → 0.0.2.81__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cycls-0.0.2.81/PKG-INFO +276 -0
- cycls-0.0.2.81/README.md +250 -0
- cycls-0.0.2.81/cycls/__init__.py +20 -0
- cycls-0.0.2.81/cycls/auth.py +4 -0
- cycls-0.0.2.81/cycls/chat.py +104 -0
- cycls-0.0.2.81/cycls/default-theme/assets/index-C2r4Daz3.js +435 -0
- cycls-0.0.2.81/cycls/default-theme/assets/index-DWGS8zpa.css +1 -0
- cycls-0.0.2.81/cycls/default-theme/index.html +28 -0
- cycls-0.0.2.81/cycls/dev-theme/index.html +298 -0
- cycls-0.0.2.81/cycls/grpc/__init__.py +3 -0
- cycls-0.0.2.81/cycls/grpc/client.py +71 -0
- cycls-0.0.2.81/cycls/grpc/runtime.proto +18 -0
- cycls-0.0.2.81/cycls/grpc/runtime_pb2.py +40 -0
- cycls-0.0.2.81/cycls/grpc/runtime_pb2_grpc.py +100 -0
- cycls-0.0.2.81/cycls/grpc/server.py +60 -0
- cycls-0.0.2.81/cycls/runtime.py +460 -0
- cycls-0.0.2.81/cycls/sdk.py +186 -0
- cycls-0.0.2.81/cycls/web.py +135 -0
- cycls-0.0.2.81/pyproject.toml +41 -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.81/PKG-INFO
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cycls
|
|
3
|
+
Version: 0.0.2.81
|
|
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
|
+
Provides-Extra: modal
|
|
16
|
+
Requires-Dist: cloudpickle (>=3.1.1,<4.0.0)
|
|
17
|
+
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
|
18
|
+
Requires-Dist: fastapi (>=0.111.0,<0.112.0)
|
|
19
|
+
Requires-Dist: grpcio (>=1.76.0,<2.0.0)
|
|
20
|
+
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
|
21
|
+
Requires-Dist: modal (>=1.1.0,<2.0.0) ; extra == "modal"
|
|
22
|
+
Requires-Dist: protobuf (>=6.0,<7.0)
|
|
23
|
+
Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<h3 align="center">
|
|
27
|
+
Distribute Intelligence
|
|
28
|
+
</h3>
|
|
29
|
+
|
|
30
|
+
<h4 align="center">
|
|
31
|
+
<a href="https://cycls.com">Website</a> |
|
|
32
|
+
<a href="https://docs.cycls.com">Docs</a>
|
|
33
|
+
</h4>
|
|
34
|
+
|
|
35
|
+
<h4 align="center">
|
|
36
|
+
<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>
|
|
37
|
+
<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>
|
|
38
|
+
<a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
|
|
39
|
+
<a href="https://x.com/cyclsai">
|
|
40
|
+
<img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
|
|
41
|
+
</a>
|
|
42
|
+
</h4>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# Cycls
|
|
47
|
+
|
|
48
|
+
The open-source SDK for distributing AI agents.
|
|
49
|
+
|
|
50
|
+
The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
|
|
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.agent(pip=["openai"])
|
|
62
|
+
async def agent(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
|
+
agent.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
|
+
agent.local() # Development with hot-reload (localhost:8080)
|
|
103
|
+
agent.local(watch=False) # Development without hot-reload
|
|
104
|
+
agent.deploy() # Production: https://agent.cycls.ai
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Get an API key at [cycls.com](https://cycls.com).
|
|
108
|
+
|
|
109
|
+
## Authentication & Analytics
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
@cycls.agent(pip=["openai"], auth=True, analytics=True)
|
|
113
|
+
async def agent(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.agent()
|
|
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.agent()
|
|
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.agent(
|
|
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_agent(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.agent(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-agent.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.81/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
|
|
26
|
+
|
|
27
|
+
## Distribute Intelligence
|
|
28
|
+
|
|
29
|
+
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import cycls
|
|
33
|
+
|
|
34
|
+
cycls.api_key = "YOUR_CYCLS_API_KEY"
|
|
35
|
+
|
|
36
|
+
@cycls.agent(pip=["openai"])
|
|
37
|
+
async def agent(context):
|
|
38
|
+
from openai import AsyncOpenAI
|
|
39
|
+
client = AsyncOpenAI()
|
|
40
|
+
|
|
41
|
+
stream = await client.responses.create(
|
|
42
|
+
model="o3-mini",
|
|
43
|
+
input=context.messages,
|
|
44
|
+
stream=True,
|
|
45
|
+
reasoning={"effort": "medium", "summary": "auto"},
|
|
46
|
+
)
|
|
47
|
+
|
|
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
|
|
53
|
+
|
|
54
|
+
agent.deploy() # Live at https://agent.cycls.ai
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install cycls
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires Docker.
|
|
64
|
+
|
|
65
|
+
## What You Get
|
|
66
|
+
|
|
67
|
+
- **Streaming API** - OpenAI-compatible `/chat/completions` endpoint
|
|
68
|
+
- **Web Interface** - Chat UI served automatically
|
|
69
|
+
- **Authentication** - `auth=True` enables JWT-based access control
|
|
70
|
+
- **Analytics** - `analytics=True` tracks usage
|
|
71
|
+
- **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
|
|
72
|
+
- **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
|
|
73
|
+
|
|
74
|
+
## Running
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
agent.local() # Development with hot-reload (localhost:8080)
|
|
78
|
+
agent.local(watch=False) # Development without hot-reload
|
|
79
|
+
agent.deploy() # Production: https://agent.cycls.ai
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Get an API key at [cycls.com](https://cycls.com).
|
|
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
|
+
|
|
100
|
+
## Native UI Components
|
|
101
|
+
|
|
102
|
+
Yield structured objects for rich streaming responses:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@cycls.agent()
|
|
106
|
+
async def demo(context):
|
|
107
|
+
yield {"type": "thinking", "thinking": "Analyzing the request..."}
|
|
108
|
+
yield "Here's what I found:\n\n"
|
|
109
|
+
|
|
110
|
+
yield {"type": "table", "headers": ["Name", "Status"]}
|
|
111
|
+
yield {"type": "table", "row": ["Server 1", "Online"]}
|
|
112
|
+
yield {"type": "table", "row": ["Server 2", "Offline"]}
|
|
113
|
+
|
|
114
|
+
yield {"type": "code", "code": "result = analyze(data)", "language": "python"}
|
|
115
|
+
yield {"type": "callout", "callout": "Analysis complete!", "style": "success"}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Component | Streaming |
|
|
119
|
+
|-----------|-----------|
|
|
120
|
+
| `{"type": "thinking", "thinking": "..."}` | Yes |
|
|
121
|
+
| `{"type": "code", "code": "...", "language": "..."}` | Yes |
|
|
122
|
+
| `{"type": "table", "headers": [...]}` | Yes |
|
|
123
|
+
| `{"type": "table", "row": [...]}` | Yes |
|
|
124
|
+
| `{"type": "status", "status": "..."}` | Yes |
|
|
125
|
+
| `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
|
|
126
|
+
| `{"type": "image", "src": "..."}` | Yes |
|
|
127
|
+
|
|
128
|
+
### Thinking Bubbles
|
|
129
|
+
|
|
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:
|
|
131
|
+
|
|
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."}
|
|
137
|
+
|
|
138
|
+
# Then output the response
|
|
139
|
+
yield "Here's what I found..."
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
|
|
143
|
+
|
|
144
|
+
## Context Object
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
@cycls.agent()
|
|
148
|
+
async def chat(context):
|
|
149
|
+
context.messages # [{"role": "user", "content": "..."}]
|
|
150
|
+
context.messages.raw # Full data including UI component parts
|
|
151
|
+
context.user # User(id, email, name, plans) when auth=True
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## API Endpoints
|
|
155
|
+
|
|
156
|
+
| Endpoint | Format |
|
|
157
|
+
|----------|--------|
|
|
158
|
+
| `POST chat/cycls` | Cycls streaming protocol |
|
|
159
|
+
| `POST chat/completions` | OpenAI-compatible |
|
|
160
|
+
|
|
161
|
+
## Streaming Protocol
|
|
162
|
+
|
|
163
|
+
Cycls streams structured components over SSE:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
data: {"type": "thinking", "thinking": "Let me "}
|
|
167
|
+
data: {"type": "thinking", "thinking": "analyze..."}
|
|
168
|
+
data: {"type": "text", "text": "Here's the answer"}
|
|
169
|
+
data: {"type": "callout", "callout": "Done!", "style": "success"}
|
|
170
|
+
data: [DONE]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integration.
|
|
174
|
+
|
|
175
|
+
## Declarative Infrastructure
|
|
176
|
+
|
|
177
|
+
Define your entire runtime in the decorator:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
@cycls.agent(
|
|
181
|
+
pip=["openai", "pandas", "numpy"],
|
|
182
|
+
apt=["ffmpeg", "libmagic1"],
|
|
183
|
+
copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
|
|
184
|
+
copy_public=["./assets/logo.png", "./static/"],
|
|
185
|
+
)
|
|
186
|
+
async def my_agent(context):
|
|
187
|
+
...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `pip` - Python Packages
|
|
191
|
+
|
|
192
|
+
Install any packages from PyPI. These are installed during the container build.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
pip=["openai", "pandas", "numpy", "transformers"]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `apt` - System Packages
|
|
199
|
+
|
|
200
|
+
Install system-level dependencies via apt-get. Need ffmpeg for audio processing? ImageMagick for images? Just declare it.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
apt=["ffmpeg", "imagemagick", "libpq-dev"]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `copy` - Bundle Files and Directories
|
|
207
|
+
|
|
208
|
+
Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
copy=[
|
|
212
|
+
"./utils.py", # Single file, relative path
|
|
213
|
+
"./models/", # Entire directory
|
|
214
|
+
"/home/user/configs/app.json", # Absolute path
|
|
215
|
+
]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Then import them in your function:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
@cycls.agent(copy=["./utils.py"])
|
|
222
|
+
async def chat(context):
|
|
223
|
+
from utils import helper_function # Your bundled module
|
|
224
|
+
...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `copy_public` - Static Files
|
|
228
|
+
|
|
229
|
+
Files and directories served at the `/public` endpoint. Perfect for images, downloads, or any static assets your agent needs to reference.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
copy_public=["./assets/logo.png", "./downloads/"]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Access them at `https://your-agent.cycls.ai/public/logo.png`.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### What You Get
|
|
240
|
+
|
|
241
|
+
- **One file** - Code, dependencies, configuration, and infrastructure together
|
|
242
|
+
- **Instant deploys** - Unchanged code deploys in seconds from cache
|
|
243
|
+
- **No drift** - What you see is what runs. Always.
|
|
244
|
+
- **Just works** - Closures, lambdas, dynamic imports - your function runs exactly as written
|
|
245
|
+
|
|
246
|
+
No YAML. No Dockerfiles. No infrastructure repo. The code is the deployment.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -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,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cycls chat - Claude Code style CLI for cycls agents"""
|
|
3
|
+
|
|
4
|
+
import json, os, re, sys
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
|
|
8
|
+
BLUE, GREEN, YELLOW, RED = "\033[34m", "\033[32m", "\033[33m", "\033[31m"
|
|
9
|
+
CALLOUTS = {"success": ("✓", GREEN), "warning": ("⚠", YELLOW), "info": ("ℹ", BLUE), "error": ("✗", RED)}
|
|
10
|
+
|
|
11
|
+
separator = lambda: f"{DIM}{'─' * min(os.get_terminal_size().columns if sys.stdout.isatty() else 80, 80)}{RESET}"
|
|
12
|
+
markdown = lambda text: re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
|
|
13
|
+
header = lambda title, meta, color=GREEN, dim=False: print(f"{color}●{RESET} {BOLD}{title}{RESET}\n ⎿ {meta}{DIM if dim else ''}", flush=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def table(headers, rows):
|
|
17
|
+
if not headers: return
|
|
18
|
+
widths = [max(len(str(h)), *(len(str(r[i])) for r in rows if i < len(r))) for i, h in enumerate(headers)]
|
|
19
|
+
line = lambda left, mid, right: left + mid.join("─" * (w + 2) for w in widths) + right
|
|
20
|
+
row = lambda cells, bold=False: "│" + "│".join(f" {BOLD if bold else ''}{str(cells[i] if i < len(cells) else '').ljust(widths[i])}{RESET if bold else ''} " for i in range(len(widths))) + "│"
|
|
21
|
+
print(f"{line('┌', '┬', '┐')}\n{row(headers, True)}\n{line('├', '┼', '┤')}")
|
|
22
|
+
for r in rows: print(row(r))
|
|
23
|
+
print(line("└", "┴", "┘"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def chat(url):
|
|
27
|
+
messages, endpoint = [], f"{url.rstrip('/')}/chat/cycls"
|
|
28
|
+
print(f"\n{BOLD}cycls{RESET} {DIM}|{RESET} {url}\n")
|
|
29
|
+
|
|
30
|
+
while True:
|
|
31
|
+
try:
|
|
32
|
+
print(separator())
|
|
33
|
+
user_input = input(f"{BOLD}{BLUE}❯{RESET} ").strip()
|
|
34
|
+
print(separator())
|
|
35
|
+
|
|
36
|
+
if not user_input: continue
|
|
37
|
+
if user_input in ("/q", "exit", "quit"): break
|
|
38
|
+
if user_input == "/c": messages, _ = [], print(f"{GREEN}⏺ Cleared{RESET}"); continue
|
|
39
|
+
|
|
40
|
+
messages.append({"role": "user", "content": user_input})
|
|
41
|
+
block, tbl = None, ([], [])
|
|
42
|
+
|
|
43
|
+
def close():
|
|
44
|
+
nonlocal block, tbl
|
|
45
|
+
if block == "thinking": print(RESET)
|
|
46
|
+
if block == "text": print()
|
|
47
|
+
if block == "table" and tbl[0]: table(*tbl); tbl = ([], [])
|
|
48
|
+
if block: print()
|
|
49
|
+
block = None
|
|
50
|
+
|
|
51
|
+
with httpx.stream("POST", endpoint, json={"messages": messages}, timeout=None) as response:
|
|
52
|
+
for line in response.iter_lines():
|
|
53
|
+
if not line.startswith("data: ") or line == "data: [DONE]": continue
|
|
54
|
+
data = json.loads(line[6:])
|
|
55
|
+
type = data.get("type")
|
|
56
|
+
|
|
57
|
+
if type is None:
|
|
58
|
+
print(markdown(data if isinstance(data, str) else data.get("text", "")), end="", flush=True); continue
|
|
59
|
+
|
|
60
|
+
if type != block: close()
|
|
61
|
+
|
|
62
|
+
if type in ("thinking", "text"):
|
|
63
|
+
if block != type: header(type.capitalize(), "Live", dim=(type == "thinking")); block = type
|
|
64
|
+
print((markdown if type == "text" else str)(data.get(type, "")), end="", flush=True)
|
|
65
|
+
elif type == "code":
|
|
66
|
+
code = data.get("code", ""); header(f"Code({data.get('language', '')})", f"{code.count(chr(10))+1} lines"); print(code, flush=True); block = type
|
|
67
|
+
elif type == "status":
|
|
68
|
+
print(f"{DIM}[{data.get('status', '')}]{RESET} ", end="", flush=True)
|
|
69
|
+
elif type == "table":
|
|
70
|
+
if "headers" in data:
|
|
71
|
+
if tbl[0]: table(*tbl)
|
|
72
|
+
header("Table", f"{len(data['headers'])} cols"); tbl, block = (data["headers"], []), type
|
|
73
|
+
elif "row" in data: tbl[1].append(data["row"])
|
|
74
|
+
elif type == "callout":
|
|
75
|
+
style = data.get("style", "info"); icon, color = CALLOUTS.get(style, ("•", RESET))
|
|
76
|
+
header(style.capitalize(), f"{icon} {data.get('callout', '')}", color=color); block = type
|
|
77
|
+
elif type == "image":
|
|
78
|
+
header("Image", data.get("src", "")); block = type
|
|
79
|
+
|
|
80
|
+
close()
|
|
81
|
+
|
|
82
|
+
except KeyboardInterrupt: print()
|
|
83
|
+
except EOFError: break
|
|
84
|
+
except (httpx.ReadError, httpx.ConnectError) as e: print(f"{RED}⏺ Connection error: {e}{RESET}"); messages and messages.pop()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
if len(sys.argv) < 2:
|
|
89
|
+
print("Usage: cycls chat <url|port>")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
arg = sys.argv[1]
|
|
92
|
+
if arg.isdigit():
|
|
93
|
+
port = int(arg)
|
|
94
|
+
if not (1 <= port <= 65535):
|
|
95
|
+
print(f"Error: Invalid port {port}. Must be between 1 and 65535.")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
url = f"http://localhost:{port}"
|
|
98
|
+
else:
|
|
99
|
+
url = arg
|
|
100
|
+
chat(url)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|