cycls 0.0.2.78__py3-none-any.whl

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/web.py ADDED
@@ -0,0 +1,131 @@
1
+ import json, inspect
2
+ from pathlib import Path
3
+ from pydantic import BaseModel
4
+ from typing import Optional
5
+
6
+ class Config(BaseModel):
7
+ public_path: str = "theme"
8
+ header: str = ""
9
+ intro: str = ""
10
+ title: str = ""
11
+ prod: bool = False
12
+ auth: bool = False
13
+ plan: str = "free"
14
+ analytics: bool = False
15
+ org: Optional[str] = None
16
+ pk: str = ""
17
+ jwks: str = ""
18
+
19
+ async def openai_encoder(stream):
20
+ if inspect.isasyncgen(stream):
21
+ async for msg in stream:
22
+ if msg: yield f"data: {json.dumps({'choices': [{'delta': {'content': msg}}]})}\n\n"
23
+ else:
24
+ for msg in stream:
25
+ if msg: yield f"data: {json.dumps({'choices': [{'delta': {'content': msg}}]})}\n\n"
26
+ yield "data: [DONE]\n\n"
27
+
28
+ def sse(item):
29
+ if not item: return None
30
+ if not isinstance(item, dict): item = {"type": "text", "text": item}
31
+ return f"data: {json.dumps(item)}\n\n"
32
+
33
+ async def encoder(stream):
34
+ if inspect.isasyncgen(stream):
35
+ async for item in stream:
36
+ if msg := sse(item): yield msg
37
+ else:
38
+ for item in stream:
39
+ if msg := sse(item): yield msg
40
+ yield "data: [DONE]\n\n"
41
+
42
+ class Messages(list):
43
+ """A list that provides text-only messages by default, with .raw for full data."""
44
+ def __init__(self, raw_messages):
45
+ self._raw = raw_messages
46
+ text_messages = []
47
+ for m in raw_messages:
48
+ text_content = "".join(
49
+ p.get("text", "") for p in m.get("parts", []) if p.get("type") == "text"
50
+ )
51
+ text_messages.append({
52
+ "role": m.get("role"),
53
+ "content": m.get("content") or text_content
54
+ })
55
+ super().__init__(text_messages)
56
+
57
+ @property
58
+ def raw(self):
59
+ return self._raw
60
+
61
+ def web(func, config):
62
+ from fastapi import FastAPI, Request, HTTPException, status, Depends
63
+ from fastapi.responses import StreamingResponse
64
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
65
+ import jwt
66
+ from jwt import PyJWKClient
67
+ from pydantic import EmailStr
68
+ from typing import List, Optional, Any
69
+ from fastapi.staticfiles import StaticFiles
70
+
71
+ if isinstance(config, dict):
72
+ config = Config(**config)
73
+
74
+ jwks = PyJWKClient(config.jwks)
75
+
76
+ class User(BaseModel):
77
+ id: str
78
+ name: Optional[str] = None
79
+ email: EmailStr
80
+ org: Optional[str] = None
81
+ plans: List[str] = []
82
+
83
+ class Context(BaseModel):
84
+ messages: Any
85
+ user: Optional[User] = None
86
+
87
+ app = FastAPI()
88
+ bearer_scheme = HTTPBearer()
89
+
90
+ def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
91
+ try:
92
+ key = jwks.get_signing_key_from_jwt(bearer.credentials)
93
+ decoded = jwt.decode(bearer.credentials, key.key, algorithms=["RS256"], leeway=10)
94
+ return {"type": "user",
95
+ "user": {"id": decoded.get("id"), "name": decoded.get("name"), "email": decoded.get("email"), "org": decoded.get("org"),
96
+ "plans": decoded.get("public", {}).get("plans", [])}}
97
+ except:
98
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
99
+
100
+ @app.post("/")
101
+ @app.post("/chat/cycls")
102
+ @app.post("/chat/completions")
103
+ async def back(request: Request, jwt: Optional[dict] = Depends(validate) if config.auth else None):
104
+ data = await request.json()
105
+ messages = data.get("messages")
106
+ user_data = jwt.get("user") if jwt else None
107
+ context = Context(messages = Messages(messages), user = User(**user_data) if user_data else None)
108
+ stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
109
+ if request.url.path == "/chat/completions":
110
+ stream = openai_encoder(stream)
111
+ elif request.url.path == "/chat/cycls":
112
+ stream = encoder(stream)
113
+ return StreamingResponse(stream, media_type="text/event-stream")
114
+
115
+ @app.get("/config")
116
+ async def get_config():
117
+ return config
118
+
119
+ if Path("public").is_dir():
120
+ app.mount("/public", StaticFiles(directory="public", html=True))
121
+ app.mount("/", StaticFiles(directory=config.public_path, html=True))
122
+
123
+ return app
124
+
125
+ def serve(func, config, name, port):
126
+ import uvicorn, logging
127
+ if isinstance(config, dict):
128
+ config = Config(**config)
129
+ logging.getLogger("uvicorn.error").addFilter(lambda r: "0.0.0.0" not in r.getMessage())
130
+ print(f"\n🔨 {name} => http://localhost:{port}\n")
131
+ uvicorn.run(web(func, config), host="0.0.0.0", port=port)
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: cycls
3
+ Version: 0.0.2.78
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
+ ## Distribute Intelligence
51
+
52
+ Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
53
+
54
+ ```python
55
+ import cycls
56
+
57
+ cycls.api_key = "YOUR_CYCLS_API_KEY"
58
+
59
+ @cycls.agent(pip=["openai"])
60
+ async def agent(context):
61
+ from openai import AsyncOpenAI
62
+ client = AsyncOpenAI()
63
+
64
+ stream = await client.responses.create(
65
+ model="o3-mini",
66
+ input=context.messages,
67
+ stream=True,
68
+ reasoning={"effort": "medium", "summary": "auto"},
69
+ )
70
+
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
76
+
77
+ agent.deploy() # Live at https://agent.cycls.ai
78
+ ```
79
+
80
+ ## Installation
81
+
82
+ ```bash
83
+ pip install cycls
84
+ ```
85
+
86
+ Requires Docker.
87
+
88
+ ## What You Get
89
+
90
+ - **Streaming API** - OpenAI-compatible `/chat/completions` endpoint
91
+ - **Web Interface** - Chat UI served automatically
92
+ - **Authentication** - `auth=True` enables JWT-based access control
93
+ - **Analytics** - `analytics=True` tracks usage
94
+ - **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
95
+ - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
96
+
97
+ ## Running
98
+
99
+ ```python
100
+ agent.local() # Development with hot-reload (localhost:8080)
101
+ agent.local(watch=False) # Development without hot-reload
102
+ agent.deploy() # Production: https://agent.cycls.ai
103
+ ```
104
+
105
+ Get an API key at [cycls.com](https://cycls.com).
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
+
123
+ ## Native UI Components
124
+
125
+ Yield structured objects for rich streaming responses:
126
+
127
+ ```python
128
+ @cycls.agent()
129
+ async def demo(context):
130
+ yield {"type": "thinking", "thinking": "Analyzing the request..."}
131
+ yield "Here's what I found:\n\n"
132
+
133
+ yield {"type": "table", "headers": ["Name", "Status"]}
134
+ yield {"type": "table", "row": ["Server 1", "Online"]}
135
+ yield {"type": "table", "row": ["Server 2", "Offline"]}
136
+
137
+ yield {"type": "code", "code": "result = analyze(data)", "language": "python"}
138
+ yield {"type": "callout", "callout": "Analysis complete!", "style": "success"}
139
+ ```
140
+
141
+ | Component | Streaming |
142
+ |-----------|-----------|
143
+ | `{"type": "thinking", "thinking": "..."}` | Yes |
144
+ | `{"type": "code", "code": "...", "language": "..."}` | Yes |
145
+ | `{"type": "table", "headers": [...]}` | Yes |
146
+ | `{"type": "table", "row": [...]}` | Yes |
147
+ | `{"type": "status", "status": "..."}` | Yes |
148
+ | `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
149
+ | `{"type": "image", "src": "..."}` | Yes |
150
+
151
+ ### Thinking Bubbles
152
+
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:
154
+
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."}
160
+
161
+ # Then output the response
162
+ yield "Here's what I found..."
163
+ ```
164
+
165
+ This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
166
+
167
+ ## Context Object
168
+
169
+ ```python
170
+ @cycls.agent()
171
+ async def chat(context):
172
+ context.messages # [{"role": "user", "content": "..."}]
173
+ context.messages.raw # Full data including UI component parts
174
+ context.user # User(id, email, name, plans) when auth=True
175
+ ```
176
+
177
+ ## API Endpoints
178
+
179
+ | Endpoint | Format |
180
+ |----------|--------|
181
+ | `POST chat/cycls` | Cycls streaming protocol |
182
+ | `POST chat/completions` | OpenAI-compatible |
183
+
184
+ ## Streaming Protocol
185
+
186
+ Cycls streams structured components over SSE:
187
+
188
+ ```
189
+ data: {"type": "thinking", "thinking": "Let me "}
190
+ data: {"type": "thinking", "thinking": "analyze..."}
191
+ data: {"type": "text", "text": "Here's the answer"}
192
+ data: {"type": "callout", "callout": "Done!", "style": "success"}
193
+ data: [DONE]
194
+ ```
195
+
196
+ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integration.
197
+
198
+ ## Declarative Infrastructure
199
+
200
+ Define your entire runtime in the decorator:
201
+
202
+ ```python
203
+ @cycls.agent(
204
+ pip=["openai", "pandas", "numpy"],
205
+ apt=["ffmpeg", "libmagic1"],
206
+ copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
207
+ copy_public=["./assets/logo.png", "./static/"],
208
+ )
209
+ async def my_agent(context):
210
+ ...
211
+ ```
212
+
213
+ ### `pip` - Python Packages
214
+
215
+ Install any packages from PyPI. These are installed during the container build.
216
+
217
+ ```python
218
+ pip=["openai", "pandas", "numpy", "transformers"]
219
+ ```
220
+
221
+ ### `apt` - System Packages
222
+
223
+ Install system-level dependencies via apt-get. Need ffmpeg for audio processing? ImageMagick for images? Just declare it.
224
+
225
+ ```python
226
+ apt=["ffmpeg", "imagemagick", "libpq-dev"]
227
+ ```
228
+
229
+ ### `copy` - Bundle Files and Directories
230
+
231
+ Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
232
+
233
+ ```python
234
+ copy=[
235
+ "./utils.py", # Single file, relative path
236
+ "./models/", # Entire directory
237
+ "/home/user/configs/app.json", # Absolute path
238
+ ]
239
+ ```
240
+
241
+ Then import them in your function:
242
+
243
+ ```python
244
+ @cycls.agent(copy=["./utils.py"])
245
+ async def chat(context):
246
+ from utils import helper_function # Your bundled module
247
+ ...
248
+ ```
249
+
250
+ ### `copy_public` - Static Files
251
+
252
+ Files and directories served at the `/public` endpoint. Perfect for images, downloads, or any static assets your agent needs to reference.
253
+
254
+ ```python
255
+ copy_public=["./assets/logo.png", "./downloads/"]
256
+ ```
257
+
258
+ Access them at `https://your-agent.cycls.ai/public/logo.png`.
259
+
260
+ ---
261
+
262
+ ### What You Get
263
+
264
+ - **One file** - Code, dependencies, configuration, and infrastructure together
265
+ - **Instant deploys** - Unchanged code deploys in seconds from cache
266
+ - **No drift** - What you see is what runs. Always.
267
+ - **Just works** - Closures, lambdas, dynamic imports - your function runs exactly as written
268
+
269
+ No YAML. No Dockerfiles. No infrastructure repo. The code is the deployment.
270
+
271
+ ## License
272
+
273
+ MIT
274
+
@@ -0,0 +1,20 @@
1
+ cycls/__init__.py,sha256=vyI1d_8VP4XW7MliFuUs_P3O9KQxyCwQu-JkxrCyhPQ,597
2
+ cycls/auth.py,sha256=xkndHZyCfnlertMMEKerCJjf23N3fVcTRVTTSXTTuzg,247
3
+ cycls/cli.py,sha256=AKf0z7ZLau3GvBVR_IhB7agmq4nVaHkcuUafNyvv2_A,7978
4
+ cycls/default-theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlFbMwyC9WqTmSKUaKw,6546
5
+ cycls/default-theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
6
+ cycls/default-theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
7
+ cycls/dev-theme/index.html,sha256=QJBHkdNuMMiwQU7o8dN8__8YQeQB45D37D-NCXIWB2Q,11585
8
+ cycls/grpc/__init__.py,sha256=sr8UQMgJEHyBreBKV8xz8UCd0zDP5lhjXTnfkOB_yOY,63
9
+ cycls/grpc/client.py,sha256=zDFIBABXzuv_RUVn5LllppZ38C7k01RyAS8ZURBjudQ,2270
10
+ cycls/grpc/runtime.proto,sha256=B1AqrNIXOtr3Xsyzfc2Z1OCBepa6hsi4DJ4a3Pf33IQ,244
11
+ cycls/grpc/runtime_pb2.py,sha256=vEJo8FGP5aWPSDqzjZldfctduA2ojiyvoody7vpf-1w,1703
12
+ cycls/grpc/runtime_pb2_grpc.py,sha256=KFd8KqGbiNsKm8X39Q9_BPwXjeZUiDl8O_4aTlEys3k,3394
13
+ cycls/grpc/server.py,sha256=pfb4bo06NKDv0OpknqMSMjB9f8HUR41EZau1c6_XU5A,1911
14
+ cycls/runtime.py,sha256=8DqK14n5DLuyl3sHQrRqLOeCPzo6_zYP8iSiHbXH_yU,18038
15
+ cycls/sdk.py,sha256=B5_ZNGvXqqKcAGAvhk-tyr0YH8kfvCJPKl01rhetvFw,6588
16
+ cycls/web.py,sha256=_QNH8K55vTm90Z7tvcRKal5IybjkB1GY7Pf9p3qu3r8,4659
17
+ cycls-0.0.2.78.dist-info/METADATA,sha256=C7BGrOxmY41GTVXuZldlLvarXEn6ImBzfGI0DvXm6HE,8496
18
+ cycls-0.0.2.78.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
19
+ cycls-0.0.2.78.dist-info/entry_points.txt,sha256=vEhqUxFhhuzCKWtq02LbMnT3wpUqdfgcM3Yh-jjXom8,40
20
+ cycls-0.0.2.78.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cycls=cycls.cli:main
3
+