cycls 0.0.2.63__py3-none-any.whl → 0.0.2.65__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.
@@ -0,0 +1,298 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Spark - Native Components</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
11
+ <script>
12
+ tailwind.config = { darkMode: "class" };
13
+ </script>
14
+ <style>
15
+ :root {
16
+ --bg-primary: #ffffff;
17
+ --bg-secondary: #f9fafb;
18
+ --text-primary: #0d0d0d;
19
+ --text-secondary: #6b7280;
20
+ --border-color: #e5e7eb;
21
+ --accent: #10a37f;
22
+ }
23
+ .dark {
24
+ --bg-primary: #212121;
25
+ --bg-secondary: #171717;
26
+ --text-primary: #ececec;
27
+ --text-secondary: #9ca3af;
28
+ --border-color: #374151;
29
+ --accent: #10a37f;
30
+ }
31
+ body {
32
+ background: var(--bg-primary);
33
+ color: var(--text-primary);
34
+ }
35
+ .thinking-bubble {
36
+ background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
37
+ border-left: 3px solid var(--accent);
38
+ }
39
+ .dark .thinking-bubble {
40
+ background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
41
+ }
42
+ .callout-info { border-left-color: #3b82f6; background: #eff6ff; }
43
+ .callout-warning { border-left-color: #f59e0b; background: #fffbeb; }
44
+ .callout-error { border-left-color: #ef4444; background: #fef2f2; }
45
+ .callout-success { border-left-color: #10b981; background: #ecfdf5; }
46
+ .dark .callout-info { background: #1e3a5f; }
47
+ .dark .callout-warning { background: #422006; }
48
+ .dark .callout-error { background: #450a0a; }
49
+ .dark .callout-success { background: #064e3b; }
50
+ @keyframes pulse-border {
51
+ 0%, 100% { border-color: var(--accent); }
52
+ 50% { border-color: transparent; }
53
+ }
54
+ .streaming { animation: pulse-border 1s infinite; }
55
+ </style>
56
+ </head>
57
+ <body class="dark">
58
+ <div id="app" class="min-h-screen flex flex-col">
59
+ <!-- Header -->
60
+ <header class="border-b border-[var(--border-color)] p-4">
61
+ <div class="max-w-3xl mx-auto flex items-center justify-between">
62
+ <h1 class="text-xl font-semibold">Spark</h1>
63
+ <button onclick="toggleDark()" class="p-2 rounded hover:bg-[var(--bg-secondary)]">
64
+ <span id="theme-icon">🌙</span>
65
+ </button>
66
+ </div>
67
+ </header>
68
+
69
+ <!-- Messages -->
70
+ <main class="flex-1 overflow-y-auto p-4">
71
+ <div id="messages" class="max-w-3xl mx-auto space-y-4"></div>
72
+ </main>
73
+
74
+ <!-- Input -->
75
+ <footer class="border-t border-[var(--border-color)] p-4">
76
+ <form id="chat-form" class="max-w-3xl mx-auto flex gap-2">
77
+ <input
78
+ type="text"
79
+ id="input"
80
+ placeholder="Send a message..."
81
+ class="flex-1 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] px-4 py-3 text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
82
+ />
83
+ <button
84
+ type="submit"
85
+ class="rounded-lg bg-[var(--accent)] px-6 py-3 text-white font-medium hover:opacity-90"
86
+ >
87
+ Send
88
+ </button>
89
+ </form>
90
+ </footer>
91
+ </div>
92
+
93
+ <script>
94
+ // State
95
+ let messages = [];
96
+ let isDark = true;
97
+
98
+ // Toggle dark mode
99
+ function toggleDark() {
100
+ isDark = !isDark;
101
+ document.body.classList.toggle('dark', isDark);
102
+ document.getElementById('theme-icon').textContent = isDark ? '🌙' : '☀️';
103
+ }
104
+
105
+ // Native component renderers
106
+ const components = {
107
+ text: (props) => marked.parse(props.text || '', { breaks: true }),
108
+
109
+ thinking: (props) => `
110
+ <div class="thinking-bubble rounded-lg p-4 my-3 italic text-[var(--text-secondary)]">
111
+ <div class="flex items-center gap-2 mb-2 text-sm font-medium text-[var(--accent)]">
112
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
114
+ </svg>
115
+ Thinking
116
+ </div>
117
+ <div>${props.thinking}</div>
118
+ </div>
119
+ `,
120
+
121
+ table: (props) => `
122
+ <div class="overflow-x-auto my-3">
123
+ <table class="min-w-full border border-[var(--border-color)] rounded-lg overflow-hidden">
124
+ ${props.headers ? `
125
+ <thead class="bg-[var(--bg-secondary)]">
126
+ <tr>
127
+ ${props.headers.map(h => `<th class="px-4 py-2 text-left font-medium">${h}</th>`).join('')}
128
+ </tr>
129
+ </thead>
130
+ ` : ''}
131
+ <tbody>
132
+ ${(props.rows || []).map((row, i) => `
133
+ <tr class="${i % 2 ? 'bg-[var(--bg-secondary)]' : ''}">
134
+ ${row.map(cell => `<td class="px-4 py-2 border-t border-[var(--border-color)]">${cell}</td>`).join('')}
135
+ </tr>
136
+ `).join('')}
137
+ </tbody>
138
+ </table>
139
+ </div>
140
+ `,
141
+
142
+ code: (props) => {
143
+ const highlighted = props.language
144
+ ? hljs.highlight(props.code, { language: props.language }).value
145
+ : hljs.highlightAuto(props.code).value;
146
+ return `
147
+ <div class="my-3 rounded-lg overflow-hidden border border-[var(--border-color)]">
148
+ <div class="bg-[var(--bg-secondary)] px-4 py-2 text-xs text-[var(--text-secondary)] flex justify-between items-center">
149
+ <span>${props.language || 'code'}</span>
150
+ <button onclick="copyCode(this)" class="hover:text-[var(--accent)]">Copy</button>
151
+ </div>
152
+ <pre class="p-4 overflow-x-auto bg-[#0d1117]"><code class="text-sm">${highlighted}</code></pre>
153
+ </div>
154
+ `;
155
+ },
156
+
157
+ callout: (props) => `
158
+ <div class="callout-${props.style || 'info'} border-l-4 rounded-r-lg p-4 my-3">
159
+ ${props.title ? `<div class="font-semibold mb-1">${props.title}</div>` : ''}
160
+ <div class="text-sm">${props.callout}</div>
161
+ </div>
162
+ `,
163
+
164
+ image: (props) => `
165
+ <div class="my-3">
166
+ <img src="${props.src}" alt="${props.alt || ''}" class="rounded-lg max-w-full" />
167
+ ${props.caption ? `<p class="text-sm text-[var(--text-secondary)] mt-2 text-center">${props.caption}</p>` : ''}
168
+ </div>
169
+ `,
170
+
171
+ button: (props) => `
172
+ <button
173
+ onclick="handleComponentAction('${props.action || ''}', ${JSON.stringify(props.payload || {})})"
174
+ class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--accent)] text-white font-medium hover:opacity-90 my-2"
175
+ >
176
+ ${props.label}
177
+ </button>
178
+ `
179
+ };
180
+
181
+ // Copy code helper
182
+ function copyCode(btn) {
183
+ const code = btn.closest('.rounded-lg').querySelector('code').textContent;
184
+ navigator.clipboard.writeText(code);
185
+ btn.textContent = 'Copied!';
186
+ setTimeout(() => btn.textContent = 'Copy', 2000);
187
+ }
188
+
189
+ // Component action handler (for interactive components)
190
+ function handleComponentAction(action, payload) {
191
+ console.log('Component action:', action, payload);
192
+ // Could send back to server or handle locally
193
+ }
194
+
195
+ // Render a single message
196
+ function renderMessage(msg) {
197
+ const wrapper = document.createElement('div');
198
+ wrapper.className = `rounded-lg p-4 ${msg.role === 'user'
199
+ ? 'bg-[var(--accent)] text-white ml-12'
200
+ : 'bg-[var(--bg-secondary)] mr-12'}`;
201
+
202
+ if (msg.role === 'user') {
203
+ wrapper.innerHTML = `<div>${msg.content}</div>`;
204
+ } else {
205
+ let html = '';
206
+ for (const part of msg.parts || [])
207
+ html += components[part.type]?.(part) || '';
208
+ wrapper.innerHTML = `<div class="prose prose-invert max-w-none">${html}</div>`;
209
+ }
210
+ return wrapper;
211
+ }
212
+
213
+ // Render all messages
214
+ function render() {
215
+ const container = document.getElementById('messages');
216
+ container.innerHTML = '';
217
+ messages.forEach(msg => container.appendChild(renderMessage(msg)));
218
+ container.scrollTop = container.scrollHeight;
219
+ }
220
+
221
+ // Stream response from server
222
+ async function streamResponse(userMessage) {
223
+ messages.push({ role: 'user', content: userMessage });
224
+ messages.push({ role: 'assistant', parts: [] });
225
+ render();
226
+
227
+ const response = await fetch('/chat/cycls', {
228
+ method: 'POST',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify({
231
+ messages: messages.slice(0, -1).map(m => ({ role: m.role, content: m.content, parts: m.parts }))
232
+ })
233
+ });
234
+
235
+ const reader = response.body.getReader();
236
+ const decoder = new TextDecoder();
237
+ let buffer = '';
238
+ let assistantMsg = messages[messages.length - 1];
239
+ let currentPart = null;
240
+
241
+ while (true) {
242
+ const { done, value } = await reader.read();
243
+ if (done) break;
244
+
245
+ buffer += decoder.decode(value, { stream: true });
246
+ const lines = buffer.split('\n');
247
+ buffer = lines.pop() || '';
248
+
249
+ for (const line of lines) {
250
+ if (!line.startsWith('data: ')) continue;
251
+ const data = line.slice(6);
252
+ if (data === '[DONE]') continue;
253
+
254
+ try {
255
+ const item = JSON.parse(data);
256
+ const type = item.type;
257
+
258
+ // Same type as current? Append content
259
+ if (currentPart && currentPart.type === type) {
260
+ if (item.row) currentPart.rows.push(item.row);
261
+ else if (item[type]) currentPart[type] = (currentPart[type] || '') + item[type];
262
+ } else {
263
+ // New component
264
+ currentPart = { ...item };
265
+ if (item.headers) currentPart.rows = [];
266
+ assistantMsg.parts.push(currentPart);
267
+ }
268
+ render();
269
+ } catch (e) {
270
+ console.error('Parse error:', e, data);
271
+ }
272
+ }
273
+ }
274
+ assistantMsg.parts = assistantMsg.parts.filter(p =>
275
+ p.type !== 'text' || p.text?.trim()
276
+ );
277
+ render();
278
+ console.log(messages);
279
+ }
280
+
281
+ // Form submit
282
+ document.getElementById('chat-form').addEventListener('submit', async (e) => {
283
+ e.preventDefault();
284
+ const input = document.getElementById('input');
285
+ const message = input.value.trim();
286
+ if (!message) return;
287
+ input.value = '';
288
+ await streamResponse(message);
289
+ });
290
+
291
+ // Highlight code blocks after render
292
+ const observer = new MutationObserver(() => {
293
+ document.querySelectorAll('pre code:not(.hljs)').forEach(el => hljs.highlightElement(el));
294
+ });
295
+ observer.observe(document.getElementById('messages'), { childList: true, subtree: true });
296
+ </script>
297
+ </body>
298
+ </html>
cycls/sdk.py CHANGED
@@ -6,6 +6,19 @@ import importlib.resources
6
6
 
7
7
  CYCLS_PATH = importlib.resources.files('cycls')
8
8
 
9
+ themes = {
10
+ "default": CYCLS_PATH.joinpath('default-theme'),
11
+ "dev": CYCLS_PATH.joinpath('dev-theme'),
12
+ }
13
+
14
+ def resolve_theme(theme):
15
+ """Resolve theme - accepts string name or path"""
16
+ if isinstance(theme, str):
17
+ if theme in themes:
18
+ return themes[theme]
19
+ raise ValueError(f"Unknown theme: {theme}. Available: {list(themes.keys())}")
20
+ return theme
21
+
9
22
  def function(python_version=None, pip=None, apt=None, run_commands=None, copy=None, name=None, base_url=None, key=None):
10
23
  # """
11
24
  # A decorator factory that transforms a Python function into a containerized,
@@ -17,9 +30,9 @@ def function(python_version=None, pip=None, apt=None, run_commands=None, copy=No
17
30
  return decorator
18
31
 
19
32
  class Agent:
20
- def __init__(self, theme=CYCLS_PATH.joinpath('theme'), org=None, api_token=None, pip=[], apt=[], copy=[], copy_public=[], modal_keys=["",""], key=None, base_url=None):
33
+ def __init__(self, theme="default", org=None, api_token=None, pip=[], apt=[], copy=[], copy_public=[], modal_keys=["",""], key=None, base_url=None):
21
34
  self.org, self.api_token = org, api_token
22
- self.theme = theme
35
+ self.theme = resolve_theme(theme)
23
36
  self.key, self.modal_keys, self.pip, self.apt, self.copy, self.copy_public = key, modal_keys, pip, apt, copy, copy_public
24
37
  self.base_url = base_url
25
38
 
@@ -72,16 +85,8 @@ class Agent:
72
85
  copy.update({i:i for i in self.copy})
73
86
  copy.update({i:f"public/{i}" for i in self.copy_public})
74
87
 
75
- def server(port):
76
- import uvicorn, logging
77
- # This one-liner hides the confusing "0.0.0.0" message
78
- logging.getLogger("uvicorn.error").addFilter(type("F",(),{"filter": lambda s,r: "0.0.0.0" not in r.getMessage()})())
79
- print(f"\n🔨 Visit {i['name']} => http://localhost:{port}\n")
80
- uvicorn.run(__import__("web").web(i["func"], *i["config"]), host="0.0.0.0", port=port)
81
-
82
88
  new = Runtime(
83
- # func=lambda port: __import__("uvicorn").run(__import__("web").web(i["func"], *i["config"]), host="0.0.0.0", port=port),
84
- func=server,
89
+ func=lambda port: __import__("web").serve(i["func"], i["config"], i["name"], port),
85
90
  name=i["name"],
86
91
  apt_packages=self.apt,
87
92
  pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
cycls/web.py CHANGED
@@ -1,63 +1,65 @@
1
1
  import json, inspect
2
2
  from pathlib import Path
3
3
 
4
- async def async_openai_encoder(stream): # clean up the meta data / new API?
5
- async for message in stream:
6
- payload = {"id": "chatcmpl-123",
7
- "object": "chat.completion.chunk",
8
- "created": 1728083325,
9
- "model": "model-1-2025-01-01",
10
- "system_fingerprint": "fp_123456",
11
- "choices": [{"delta": {"content": message}}]}
12
- if message:
13
- yield f"data: {json.dumps(payload)}\n\n"
4
+ JWKS_PROD = "https://clerk.cycls.ai/.well-known/jwks.json"
5
+ PK_LIVE = "pk_live_Y2xlcmsuY3ljbHMuYWkk"
6
+ JWKS_TEST = "https://select-sloth-58.clerk.accounts.dev/.well-known/jwks.json"
7
+ PK_TEST = "pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
8
+
9
+ async def openai_encoder(stream):
10
+ if inspect.isasyncgen(stream):
11
+ async for msg in stream:
12
+ if msg: yield f"data: {json.dumps({'choices': [{'delta': {'content': msg}}]})}\n\n"
13
+ else:
14
+ for msg in stream:
15
+ if msg: yield f"data: {json.dumps({'choices': [{'delta': {'content': msg}}]})}\n\n"
14
16
  yield "data: [DONE]\n\n"
15
17
 
16
- def openai_encoder(stream):
17
- for message in stream:
18
- payload = {"id": "chatcmpl-123",
19
- "object": "chat.completion.chunk",
20
- "created": 1728083325,
21
- "model": "model-1-2025-01-01",
22
- "system_fingerprint": "fp_123456",
23
- "choices": [{"delta": {"content": message}}]}
24
- if message:
25
- yield f"data: {json.dumps(payload)}\n\n"
18
+ def sse(item):
19
+ if not item: return None
20
+ if not isinstance(item, dict): item = {"type": "text", "text": item}
21
+ return f"data: {json.dumps(item)}\n\n"
22
+
23
+ async def encoder(stream):
24
+ if inspect.isasyncgen(stream):
25
+ async for item in stream:
26
+ if msg := sse(item): yield msg
27
+ else:
28
+ for item in stream:
29
+ if msg := sse(item): yield msg
26
30
  yield "data: [DONE]\n\n"
27
31
 
28
- test_auth_public_key = """
29
- -----BEGIN PUBLIC KEY-----
30
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyDudrDtQ5irw6hPWf2rw
31
- FvNAFWeOouOO3XNWVQrjXCZfegiLYkL4cJdm4eqIuMdFHGnXU+gWT5P0EkLIkbtE
32
- zpqDb5Wp27WpSRb5lqJehpU7FE+oQuovCwR9m5gYXP5rfM+CQ7ZPw/CcOQPtOB5G
33
- 0UijBhmYqws3SFp1Rk1uFed1F/esspt6Ifq2uDSHESleylqTKUCQiBa++z4wllcV
34
- PbNiooLRpsF0kGljP2dXXy/ViF7q9Cblgl+FdrqtGfHD+DHJuOSYcPnRa0IHZYS4
35
- r5i9C2lejVrEDqgJk5IbmQgez0wmEG4ynAxiDLvfdtvrd27PyBI75FsyLER/ydBH
36
- WwIDAQAB
37
- -----END PUBLIC KEY-----
38
- """
39
-
40
- live_auth_public_key = """
41
- -----BEGIN PUBLIC KEY-----
42
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAorfL7XyxrLG/X+Kq9ImY
43
- oSQ+Y3PY5qi8t8R4urY9u4ADJ48j9LkmFz8ALbubQkl3IByDDuVbka49m8id9isy
44
- F9ZJErsZzzlYztrgI5Sg4R6OJXcNWLqh/tzutMWJFOrE3LnHXpeyQMo/6qAd59Dx
45
- sNqzGxBTGPV1BZvpfhp/TT/sjgbPQWHS4PMpKD4vZLKXeTNJ913fMTUoFAIaL0sT
46
- EhoeLUwvIuhLx4UYTmjO/sa+fS6mdghjddOkjSS/AWr/K8mN3IXDImGqh83L7/P0
47
- RCru4Hvarm0qPIhfwEFfWhKFXONMj3x2fT4MM1Uw1H7qKTER2MtOjmdchKNX7x9b
48
- XwIDAQAB
49
- -----END PUBLIC KEY-----
50
- """
32
+ class Messages(list):
33
+ """A list that provides text-only messages by default, with .raw for full data."""
34
+ def __init__(self, raw_messages):
35
+ self._raw = raw_messages
36
+ text_messages = []
37
+ for m in raw_messages:
38
+ text_content = "".join(
39
+ p.get("text", "") for p in m.get("parts", []) if p.get("type") == "text"
40
+ )
41
+ text_messages.append({
42
+ "role": m.get("role"),
43
+ "content": m.get("content") or text_content
44
+ })
45
+ super().__init__(text_messages)
46
+
47
+ @property
48
+ def raw(self):
49
+ return self._raw
51
50
 
52
51
  def web(func, public_path="", prod=False, org=None, api_token=None, header="", intro="", title="", auth=False, tier="", analytics=False): # API auth
53
52
  from fastapi import FastAPI, Request, HTTPException, status, Depends
54
53
  from fastapi.responses import StreamingResponse , HTMLResponse
55
54
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
56
55
  import jwt
56
+ from jwt import PyJWKClient
57
57
  from pydantic import BaseModel, EmailStr
58
- from typing import List, Optional
58
+ from typing import List, Optional, Any
59
59
  from fastapi.staticfiles import StaticFiles
60
60
 
61
+ jwks = PyJWKClient(JWKS_PROD if prod else JWKS_TEST)
62
+
61
63
  class User(BaseModel):
62
64
  id: str
63
65
  name: Optional[str] = None
@@ -78,34 +80,35 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
78
80
  pk_test: str
79
81
 
80
82
  class Context(BaseModel):
81
- messages: List[dict]
83
+ messages: Any
82
84
  user: Optional[User] = None
83
85
 
84
86
  app = FastAPI()
85
87
  bearer_scheme = HTTPBearer()
86
88
 
87
89
  def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
88
- # if api_token and api_token==""
89
90
  try:
90
- public_key = live_auth_public_key if prod else test_auth_public_key
91
- decoded = jwt.decode(bearer.credentials, public_key, algorithms=["RS256"], leeway=10)
92
- # print(decoded)
93
- return {"type": "user",
91
+ key = jwks.get_signing_key_from_jwt(bearer.credentials)
92
+ decoded = jwt.decode(bearer.credentials, key.key, algorithms=["RS256"], leeway=10)
93
+ return {"type": "user",
94
94
  "user": {"id": decoded.get("id"), "name": decoded.get("name"), "email": decoded.get("email"), "org": decoded.get("org"),
95
- "plans": decoded.get("public").get("plans", [])}}
95
+ "plans": decoded.get("public", {}).get("plans", [])}}
96
96
  except:
97
97
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
98
98
 
99
99
  @app.post("/")
100
+ @app.post("/chat/cycls")
100
101
  @app.post("/chat/completions")
101
102
  async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
102
103
  data = await request.json()
103
104
  messages = data.get("messages")
104
105
  user_data = jwt.get("user") if jwt else None
105
- context = Context(messages = messages, user = User(**user_data) if user_data else None)
106
+ context = Context(messages = Messages(messages), user = User(**user_data) if user_data else None)
106
107
  stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
107
108
  if request.url.path == "/chat/completions":
108
- stream = async_openai_encoder(stream) if inspect.isasyncgen(stream) else openai_encoder(stream)
109
+ stream = openai_encoder(stream)
110
+ elif request.url.path == "/chat/cycls":
111
+ stream = encoder(stream)
109
112
  return StreamingResponse(stream, media_type="text/event-stream")
110
113
 
111
114
  @app.get("/metadata")
@@ -119,12 +122,18 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
119
122
  tier=tier,
120
123
  analytics=analytics,
121
124
  org=org,
122
- pk_live="pk_live_Y2xlcmsuY3ljbHMuYWkk",
123
- pk_test="pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
125
+ pk_live=PK_LIVE,
126
+ pk_test=PK_TEST
124
127
  )
125
128
 
126
129
  if Path("public").is_dir():
127
130
  app.mount("/public", StaticFiles(directory="public", html=True))
128
131
  app.mount("/", StaticFiles(directory=public_path, html=True))
129
132
 
130
- return app
133
+ return app
134
+
135
+ def serve(func, config, name, port):
136
+ import uvicorn, logging
137
+ logging.getLogger("uvicorn.error").addFilter(lambda r: "0.0.0.0" not in r.getMessage())
138
+ print(f"\n🔨 {name} => http://localhost:{port}\n")
139
+ uvicorn.run(web(func, *config), host="0.0.0.0", port=port)
@@ -0,0 +1,269 @@
1
+ Metadata-Version: 2.4
2
+ Name: cycls
3
+ Version: 0.0.2.65
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: httpx (>=0.27.0,<0.28.0)
20
+ Requires-Dist: modal (>=1.1.0,<2.0.0) ; extra == "modal"
21
+ Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
22
+ Description-Content-Type: text/markdown
23
+
24
+ <h3 align="center">
25
+ Distribute Intelligence
26
+ </h3>
27
+
28
+ <h4 align="center">
29
+ <a href="https://cycls.com">Website</a> |
30
+ <a href="https://docs.cycls.com">Docs</a>
31
+ </h4>
32
+
33
+ <h4 align="center">
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://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
36
+ <a href="https://x.com/cyclsai">
37
+ <img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
38
+ </a>
39
+ </h4>
40
+
41
+ ---
42
+
43
+ # Cycls
44
+
45
+ The open-source SDK for distributing AI agents.
46
+
47
+ ## Distribute Intelligence
48
+
49
+ AI capabilities shouldn't be locked in notebooks or trapped behind months of infrastructure work. Cycls turns your Python functions into production services - complete with APIs, interfaces, auth, and analytics. You focus on the intelligence. Cycls handles the distribution.
50
+
51
+ Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
52
+
53
+ ```python
54
+ import cycls
55
+
56
+ agent = cycls.Agent(pip=["openai"])
57
+
58
+ @agent("my-agent", auth=True, analytics=True)
59
+ async def chat(context):
60
+ from openai import AsyncOpenAI
61
+ client = AsyncOpenAI()
62
+
63
+ response = await client.chat.completions.create(
64
+ model="gpt-4o",
65
+ messages=context.messages,
66
+ stream=True
67
+ )
68
+
69
+ async for chunk in response:
70
+ if chunk.choices[0].delta.content:
71
+ yield chunk.choices[0].delta.content
72
+
73
+ agent.deploy(prod=True) # Live at https://my-agent.cycls.ai
74
+ ```
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ pip install cycls
80
+ ```
81
+
82
+ Requires Docker.
83
+
84
+ ## What You Get
85
+
86
+ - **Streaming API** - OpenAI-compatible `/chat/completions` endpoint
87
+ - **Web Interface** - Chat UI served automatically
88
+ - **Authentication** - `auth=True` enables JWT-based access control
89
+ - **Analytics** - `analytics=True` tracks usage
90
+ - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
91
+ - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
92
+
93
+ ## Deploying
94
+
95
+ ```python
96
+ agent.deploy(prod=False) # Development: localhost:8080
97
+ agent.deploy(prod=True) # Production: https://agent-name.cycls.ai
98
+ ```
99
+
100
+ Get an API key at [cycls.com](https://cycls.com).
101
+
102
+ ## Native UI Components
103
+
104
+ Yield structured objects for rich streaming responses:
105
+
106
+ ```python
107
+ @agent()
108
+ async def demo(context):
109
+ yield {"type": "thinking", "thinking": "Analyzing the request..."}
110
+ yield "Here's what I found:\n\n"
111
+
112
+ yield {"type": "table", "headers": ["Name", "Status"]}
113
+ yield {"type": "table", "row": ["Server 1", "Online"]}
114
+ yield {"type": "table", "row": ["Server 2", "Offline"]}
115
+
116
+ yield {"type": "code", "code": "result = analyze(data)", "language": "python"}
117
+ yield {"type": "callout", "callout": "Analysis complete!", "style": "success"}
118
+ ```
119
+
120
+ | Component | Streaming |
121
+ |-----------|-----------|
122
+ | `{"type": "thinking", "thinking": "..."}` | Yes |
123
+ | `{"type": "code", "code": "...", "language": "..."}` | Yes |
124
+ | `{"type": "table", "headers": [...]}` | Yes |
125
+ | `{"type": "table", "row": [...]}` | Yes |
126
+ | `{"type": "status", "status": "..."}` | Yes |
127
+ | `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
128
+ | `{"type": "image", "src": "..."}` | Yes |
129
+
130
+ ### Reasoning Models
131
+
132
+ ```python
133
+ @agent()
134
+ async def chat(context):
135
+ from openai import AsyncOpenAI
136
+ client = AsyncOpenAI()
137
+
138
+ stream = await client.responses.create(
139
+ model="o3-mini",
140
+ input=context.messages,
141
+ stream=True,
142
+ reasoning={"effort": "medium", "summary": "auto"},
143
+ )
144
+
145
+ async for event in stream:
146
+ if event.type == "response.reasoning_summary_text.delta":
147
+ yield {"type": "thinking", "thinking": event.delta}
148
+ elif event.type == "response.output_text.delta":
149
+ yield event.delta
150
+ ```
151
+
152
+ ## Context Object
153
+
154
+ ```python
155
+ @agent()
156
+ async def chat(context):
157
+ context.messages # [{"role": "user", "content": "..."}]
158
+ context.messages.raw # Full data including UI component parts
159
+ context.user # User(id, email, name, plans) when auth=True
160
+ ```
161
+
162
+ ## API Endpoints
163
+
164
+ | Endpoint | Format |
165
+ |----------|--------|
166
+ | `POST chat/cycls` | Cycls streaming protocol |
167
+ | `POST chat/completions` | OpenAI-compatible |
168
+
169
+ ## Streaming Protocol
170
+
171
+ Cycls streams structured components over SSE:
172
+
173
+ ```
174
+ data: {"type": "thinking", "thinking": "Let me "}
175
+ data: {"type": "thinking", "thinking": "analyze..."}
176
+ data: {"type": "text", "text": "Here's the answer"}
177
+ data: {"type": "callout", "callout": "Done!", "style": "success"}
178
+ data: [DONE]
179
+ ```
180
+
181
+ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integration.
182
+
183
+ ## Declarative Infrastructure
184
+
185
+ Define your entire runtime in Python:
186
+
187
+ ```python
188
+ agent = cycls.Agent(
189
+ pip=["openai", "pandas", "numpy"],
190
+ apt=["ffmpeg", "libmagic1"],
191
+ run_commands=["curl -sSL https://example.com/setup.sh | bash"],
192
+ copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
193
+ copy_public=["./assets/logo.png", "./static/"],
194
+ )
195
+ ```
196
+
197
+ ### `pip` - Python Packages
198
+
199
+ Install any packages from PyPI. These are installed during the container build.
200
+
201
+ ```python
202
+ pip=["openai", "pandas", "numpy", "transformers"]
203
+ ```
204
+
205
+ ### `apt` - System Packages
206
+
207
+ Install system-level dependencies via apt-get. Need ffmpeg for audio processing? ImageMagick for images? Just declare it.
208
+
209
+ ```python
210
+ apt=["ffmpeg", "imagemagick", "libpq-dev"]
211
+ ```
212
+
213
+ ### `run_commands` - Shell Commands
214
+
215
+ Run arbitrary shell commands during the container build. Useful for custom setup scripts, downloading assets, or any build-time configuration.
216
+
217
+ ```python
218
+ run_commands=[
219
+ "curl -sSL https://example.com/setup.sh | bash",
220
+ "chmod +x /app/scripts/*.sh"
221
+ ]
222
+ ```
223
+
224
+ ### `copy` - Bundle Files and Directories
225
+
226
+ Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
227
+
228
+ ```python
229
+ copy=[
230
+ "./utils.py", # Single file, relative path
231
+ "./models/", # Entire directory
232
+ "/home/user/configs/app.json", # Absolute path
233
+ ]
234
+ ```
235
+
236
+ Then import them in your function:
237
+
238
+ ```python
239
+ @agent()
240
+ async def chat(context):
241
+ from utils import helper_function # Your bundled module
242
+ ...
243
+ ```
244
+
245
+ ### `copy_public` - Static Files
246
+
247
+ Files and directories served at the `/public` endpoint. Perfect for images, downloads, or any static assets your agent needs to reference.
248
+
249
+ ```python
250
+ copy_public=["./assets/logo.png", "./downloads/"]
251
+ ```
252
+
253
+ Access them at `https://your-agent.cycls.ai/public/logo.png`.
254
+
255
+ ---
256
+
257
+ ### What You Get
258
+
259
+ - **One file** - Code, dependencies, configuration, and infrastructure together
260
+ - **Instant deploys** - Unchanged code deploys in seconds from cache
261
+ - **No drift** - What you see is what runs. Always.
262
+ - **Just works** - Closures, lambdas, dynamic imports - your function runs exactly as written
263
+
264
+ No YAML. No Dockerfiles. No infrastructure repo. The code is the deployment.
265
+
266
+ ## License
267
+
268
+ MIT
269
+
@@ -0,0 +1,11 @@
1
+ cycls/__init__.py,sha256=bVT0dYTXLdSC3ZURgtm-DEOj-VO6RUM6zGsJB0zuj6Y,61
2
+ cycls/default-theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlFbMwyC9WqTmSKUaKw,6546
3
+ cycls/default-theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
4
+ cycls/default-theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
5
+ cycls/dev-theme/index.html,sha256=QJBHkdNuMMiwQU7o8dN8__8YQeQB45D37D-NCXIWB2Q,11585
6
+ cycls/runtime.py,sha256=hLBtwtGz0FCW1-EPCJy6kMdF2fB3i6Df_H8-bm7qeK0,18223
7
+ cycls/sdk.py,sha256=9R8UYaYi44DsZyO-Nxrv-g0y5BJ1IR_4PNyzvRfIbSs,5915
8
+ cycls/web.py,sha256=_GU50yjfD7podThnivNJ0zv6mPhFLTh25xVwTwTdhXQ,5101
9
+ cycls-0.0.2.65.dist-info/METADATA,sha256=HNFesDo57HQ1bFJVVbdfhTSr0eckZZjF97-mAUofAnc,7943
10
+ cycls-0.0.2.65.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
11
+ cycls-0.0.2.65.dist-info/RECORD,,
@@ -1,140 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cycls
3
- Version: 0.0.2.63
4
- Summary: Cycls SDK
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: jwt (>=1.4.0,<2.0.0)
20
- Requires-Dist: modal (>=1.1.0,<2.0.0)
21
- Description-Content-Type: text/markdown
22
-
23
- <h3 align="center">
24
- The Distribution SDK for AI Agents.
25
- </h3>
26
-
27
- <h4 align="center">
28
- <a href="https://cycls.com">Website</a> |
29
- <a href="https://docs.cycls.com">Docs</a>
30
- </h4>
31
-
32
- <h4 align="center">
33
- <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>
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
- # Cycls 🚲
42
-
43
- `cycls` is an open-source SDK for building and publishing AI agents. With a single decorator and one command, you can deploy your code as a web application complete with a front-end UI and an OpenAI-compatible API endpoint.
44
-
45
- ## Key Features
46
-
47
- * ✨ **Zero-Config Deployment:** No YAML or Dockerfiles. `cycls` infers your dependencies, and APIs directly from your Python code.
48
- * 🚀 **One-Command Push to Cloud:** Go from local code to a globally scalable, serverless application with a single `agent.deploy()`.
49
- * 💻 **Instant Local Testing:** Run `agent.local()` to spin up a local server with hot-reloading for rapid iteration and debugging.
50
- * 🤖 **OpenAI-Compatible API:** Automatically serves a streaming `/chat/completions` endpoint.
51
- * 🌐 **Automatic Web UI:** Get a clean, interactive front-end for your agent out of the box, with no front-end code required.
52
- * 🔐 **Built-in Authentication:** Secure your agent for production with a simple `auth=True` flag that enables JWT-based authentication.
53
- * 📦 **Declarative Dependencies:** Define all your `pip`, `apt`, or local file dependencies directly in Python.
54
-
55
-
56
- ## Installation
57
-
58
- ```bash
59
- pip install cycls
60
- ```
61
-
62
- **Note:** You must have [Docker](https://www.docker.com/get-started) installed and running on your machine.
63
-
64
- ## How to Use
65
- ### 1. Local Development: "Hello, World!"
66
-
67
- Create a file main.py. This simple example creates an agent that streams back the message "hi".
68
-
69
- ```py
70
- import cycls
71
-
72
- # Initialize the agent
73
- agent = cycls.Agent()
74
-
75
- # Decorate your function to register it as an agent
76
- @agent()
77
- async def hello(context):
78
- yield "Hello, World!"
79
-
80
- agent.deploy(prod=False)
81
- ```
82
-
83
- Run it from your terminal:
84
-
85
- ```bash
86
- python main.py
87
- ```
88
- This will start a local server. Open your browser to http://localhost:8080 to interact with your agent.
89
-
90
- ### 2. Cloud Deployment: An OpenAI-Powered Agent
91
- This example creates a more advanced agent that calls the OpenAI API. It will be deployed to the cloud with authentication enabled.
92
-
93
- ```py
94
- import cycls
95
-
96
- # Initialize the agent with dependencies and API keys
97
- agent = cycls.Agent(
98
- pip=["openai"],
99
- key="YOUR_CYCLS_KEY" # Get yours from https://cycls.com
100
- )
101
-
102
- # A helper function to call the LLM
103
- async def llm(messages):
104
- # Import inside the function: 'openai' is needed at runtime in the container.
105
- import openai
106
- client = openai.AsyncOpenAI(api_key="YOUR_OPENAI_API_KEY")
107
- model = "gpt-4o"
108
- response = await client.chat.completions.create(
109
- model=model,
110
- messages=messages,
111
- temperature=1.0,
112
- stream=True
113
- )
114
- # Yield the content from the streaming response
115
- async def event_stream():
116
- async for chunk in response:
117
- content = chunk.choices[0].delta.content
118
- if content:
119
- yield content
120
- return event_stream()
121
-
122
- # Register the function as an agent named "cake" and enable auth
123
- @agent("cake", auth=True)
124
- async def cake_agent(context):
125
- # The context object contains the message history
126
- return await llm(context.messages)
127
-
128
- # Deploy the agent to the cloud
129
- agent.deploy(prod=True)
130
- ```
131
-
132
- Run the deployment command from your terminal:
133
-
134
- ```bash
135
- python main.py
136
- ```
137
- After a few moments, your agent will be live and accessible at a public URL like https://cake.cycls.ai.
138
-
139
- ### License
140
- This project is licensed under the MIT License.
@@ -1,10 +0,0 @@
1
- cycls/__init__.py,sha256=bVT0dYTXLdSC3ZURgtm-DEOj-VO6RUM6zGsJB0zuj6Y,61
2
- cycls/runtime.py,sha256=hLBtwtGz0FCW1-EPCJy6kMdF2fB3i6Df_H8-bm7qeK0,18223
3
- cycls/sdk.py,sha256=YmBg6UuQIFIPRQOufZV0yYP5H1-wittDfTOwth4J_OM,6028
4
- cycls/theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlFbMwyC9WqTmSKUaKw,6546
5
- cycls/theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
6
- cycls/theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
7
- cycls/web.py,sha256=0wu8VIBAAlDs7bCGkzYETlKiHPQ5vOr1nu4vxahKBYo,5132
8
- cycls-0.0.2.63.dist-info/METADATA,sha256=T22tqJrCEmus2JADQRDY-6oN3mPi4grvtPMMtlLX0ig,4800
9
- cycls-0.0.2.63.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
10
- cycls-0.0.2.63.dist-info/RECORD,,
File without changes
File without changes