cycls 0.0.2.62__py3-none-any.whl → 0.0.2.64__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.
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
@@ -13,8 +13,8 @@
13
13
  rel="stylesheet"
14
14
  href="https://esm.sh/katex@0.16.8/dist/katex.min.css"
15
15
  />
16
- <script type="module" crossorigin src="/assets/index-qVg4Gbap.js"></script>
17
- <link rel="stylesheet" crossorigin href="/assets/index-DWGS8zpa.css">
16
+ <script type="module" crossorigin src="/assets/index-D5EDcI4J.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index-B0ZKcm_V.css">
18
18
  </head>
19
19
  <body style="overflow-x: hidden">
20
20
  <div id="root"></div>
@@ -0,0 +1,305 @@
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.content || '', { 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.content}</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.content, { language: props.language }).value
145
+ : hljs.highlightAuto(props.content).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.type || '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.content}</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.name]?.(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
+ const decode = {
242
+ '+': ([, name, props]) => {
243
+ currentPart = {name, ...props};
244
+ if (props.headers) currentPart.rows = [];
245
+ assistantMsg.parts.push(currentPart);
246
+ },
247
+ '~': ([, props]) => {
248
+ if (!currentPart) return;
249
+ for (const [k, v] of Object.entries(props)) {
250
+ if (k === 'content') currentPart.content = (currentPart.content || '') + v;
251
+ else if (k === 'row') currentPart.rows.push(v);
252
+ else currentPart[k] = v;
253
+ }
254
+ },
255
+ '-': () => { currentPart = null; },
256
+ '=': ([, props]) => { assistantMsg.parts.push(props); }
257
+ };
258
+
259
+ while (true) {
260
+ const { done, value } = await reader.read();
261
+ if (done) break;
262
+
263
+ buffer += decoder.decode(value, { stream: true });
264
+ const lines = buffer.split('\n');
265
+ buffer = lines.pop() || '';
266
+
267
+ for (const line of lines) {
268
+ if (!line.startsWith('data: ')) continue;
269
+ const data = line.slice(6);
270
+ if (data === '[DONE]') continue;
271
+
272
+ try {
273
+ const msg = JSON.parse(data);
274
+ decode[msg[0]]?.(msg);
275
+ render();
276
+ } catch (e) {
277
+ console.error('Parse error:', e, data);
278
+ }
279
+ }
280
+ }
281
+ assistantMsg.parts = assistantMsg.parts.filter(p =>
282
+ p.name !== 'text' || p.content?.trim()
283
+ );
284
+ render();
285
+ console.log(messages);
286
+ }
287
+
288
+ // Form submit
289
+ document.getElementById('chat-form').addEventListener('submit', async (e) => {
290
+ e.preventDefault();
291
+ const input = document.getElementById('input');
292
+ const message = input.value.trim();
293
+ if (!message) return;
294
+ input.value = '';
295
+ await streamResponse(message);
296
+ });
297
+
298
+ // Highlight code blocks after render
299
+ const observer = new MutationObserver(() => {
300
+ document.querySelectorAll('pre code:not(.hljs)').forEach(el => hljs.highlightElement(el));
301
+ });
302
+ observer.observe(document.getElementById('messages'), { childList: true, subtree: true });
303
+ </script>
304
+ </body>
305
+ </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/ui.py ADDED
@@ -0,0 +1,6 @@
1
+ thinking = lambda content: {"name": "thinking", "content": content}
2
+ status = lambda content: {"name": "status", "content": content}
3
+ code = lambda content, language=None: {"name": "code", "content": content, "language": language}
4
+ table = lambda headers=None, row=None: {"name": "table", "headers": headers} if headers else {"name": "table", "row": row} if row else None
5
+ callout = lambda content, type="info", title=None: {"name": "callout", "content": content, "type": type, "title": title, "_complete": True}
6
+ image = lambda src, alt=None, caption=None: {"name": "image", "src": src, "alt": alt, "caption": caption, "_complete": True}
cycls/web.py CHANGED
@@ -1,63 +1,83 @@
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
+ class Encoder:
19
+ def __init__(self): self.cur = None
20
+ def sse(self, d): return f"data: {json.dumps(d)}\n\n"
21
+ def close(self):
22
+ if self.cur: self.cur = None; return self.sse(["-"])
23
+
24
+ def process(self, item):
25
+ if not item: return
26
+ if not isinstance(item, dict): item = {"name": "text", "content": item}
27
+ n, done = item.get("name"), item.get("_complete")
28
+ p = {k: v for k, v in item.items() if k not in ("name", "_complete")}
29
+ if done:
30
+ if c := self.close(): yield c
31
+ yield self.sse(["=", {"name": n, **p}])
32
+ elif n != self.cur:
33
+ if c := self.close(): yield c
34
+ self.cur = n
35
+ yield self.sse(["+", n, p])
36
+ else:
37
+ yield self.sse(["~", p])
38
+
39
+ async def encoder(stream):
40
+ enc = Encoder()
41
+ if inspect.isasyncgen(stream):
42
+ async for item in stream:
43
+ for msg in enc.process(item): yield msg
44
+ else:
45
+ for item in stream:
46
+ for msg in enc.process(item): yield msg
47
+ if close := enc.close(): yield close
26
48
  yield "data: [DONE]\n\n"
27
49
 
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
- """
50
+ class Messages(list):
51
+ """A list that provides text-only messages by default, with .raw for full data."""
52
+ def __init__(self, raw_messages):
53
+ self._raw = raw_messages
54
+ text_messages = []
55
+ for m in raw_messages:
56
+ text_content = "".join(
57
+ p.get("content", "") for p in m.get("parts", []) if p.get("name") == "text"
58
+ )
59
+ text_messages.append({
60
+ "role": m.get("role"),
61
+ "content": m.get("content") or text_content
62
+ })
63
+ super().__init__(text_messages)
64
+
65
+ @property
66
+ def raw(self):
67
+ return self._raw
51
68
 
52
69
  def web(func, public_path="", prod=False, org=None, api_token=None, header="", intro="", title="", auth=False, tier="", analytics=False): # API auth
53
70
  from fastapi import FastAPI, Request, HTTPException, status, Depends
54
71
  from fastapi.responses import StreamingResponse , HTMLResponse
55
72
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
56
73
  import jwt
74
+ from jwt import PyJWKClient
57
75
  from pydantic import BaseModel, EmailStr
58
- from typing import List, Optional
76
+ from typing import List, Optional, Any
59
77
  from fastapi.staticfiles import StaticFiles
60
78
 
79
+ jwks = PyJWKClient(JWKS_PROD if prod else JWKS_TEST)
80
+
61
81
  class User(BaseModel):
62
82
  id: str
63
83
  name: Optional[str] = None
@@ -78,34 +98,35 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
78
98
  pk_test: str
79
99
 
80
100
  class Context(BaseModel):
81
- messages: List[dict]
101
+ messages: Any
82
102
  user: Optional[User] = None
83
103
 
84
104
  app = FastAPI()
85
105
  bearer_scheme = HTTPBearer()
86
106
 
87
107
  def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
88
- # if api_token and api_token==""
89
108
  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",
109
+ key = jwks.get_signing_key_from_jwt(bearer.credentials)
110
+ decoded = jwt.decode(bearer.credentials, key.key, algorithms=["RS256"], leeway=10)
111
+ return {"type": "user",
94
112
  "user": {"id": decoded.get("id"), "name": decoded.get("name"), "email": decoded.get("email"), "org": decoded.get("org"),
95
- "plans": decoded.get("public").get("plans", [])}}
113
+ "plans": decoded.get("public", {}).get("plans", [])}}
96
114
  except:
97
115
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
98
116
 
99
117
  @app.post("/")
118
+ @app.post("/chat/cycls")
100
119
  @app.post("/chat/completions")
101
120
  async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
102
121
  data = await request.json()
103
122
  messages = data.get("messages")
104
123
  user_data = jwt.get("user") if jwt else None
105
- context = Context(messages = messages, user = User(**user_data) if user_data else None)
124
+ context = Context(messages = Messages(messages), user = User(**user_data) if user_data else None)
106
125
  stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
107
126
  if request.url.path == "/chat/completions":
108
- stream = async_openai_encoder(stream) if inspect.isasyncgen(stream) else openai_encoder(stream)
127
+ stream = openai_encoder(stream)
128
+ elif request.url.path == "/chat/cycls":
129
+ stream = encoder(stream)
109
130
  return StreamingResponse(stream, media_type="text/event-stream")
110
131
 
111
132
  @app.get("/metadata")
@@ -119,12 +140,18 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
119
140
  tier=tier,
120
141
  analytics=analytics,
121
142
  org=org,
122
- pk_live="pk_live_Y2xlcmsuY3ljbHMuYWkk",
123
- pk_test="pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
143
+ pk_live=PK_LIVE,
144
+ pk_test=PK_TEST
124
145
  )
125
146
 
126
147
  if Path("public").is_dir():
127
148
  app.mount("/public", StaticFiles(directory="public", html=True))
128
149
  app.mount("/", StaticFiles(directory=public_path, html=True))
129
150
 
130
- return app
151
+ return app
152
+
153
+ def serve(func, config, name, port):
154
+ import uvicorn, logging
155
+ logging.getLogger("uvicorn.error").addFilter(lambda r: "0.0.0.0" not in r.getMessage())
156
+ print(f"\n🔨 {name} => http://localhost:{port}\n")
157
+ uvicorn.run(web(func, *config), host="0.0.0.0", port=port)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.62
3
+ Version: 0.0.2.64
4
4
  Summary: Cycls SDK
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -12,12 +12,13 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
+ Provides-Extra: modal
15
16
  Requires-Dist: cloudpickle (>=3.1.1,<4.0.0)
16
17
  Requires-Dist: docker (>=7.1.0,<8.0.0)
17
18
  Requires-Dist: fastapi (>=0.111.0,<0.112.0)
18
19
  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)
20
+ Requires-Dist: modal (>=1.1.0,<2.0.0) ; extra == "modal"
21
+ Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  <h3 align="center">
@@ -0,0 +1,12 @@
1
+ cycls/__init__.py,sha256=DqHeIi5A86XmtzmGGdwVYOKahoqddL2IuzPdo01P0-4,84
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=ebDuZoyccv5VNQ1ICR6NTzjk0saAr3dJgiSHl5ctdd0,11784
6
+ cycls/runtime.py,sha256=hLBtwtGz0FCW1-EPCJy6kMdF2fB3i6Df_H8-bm7qeK0,18223
7
+ cycls/sdk.py,sha256=9R8UYaYi44DsZyO-Nxrv-g0y5BJ1IR_4PNyzvRfIbSs,5915
8
+ cycls/ui.py,sha256=FyCRgtuJFhj-NjNDWrDW0-XQsTcpdg494gHyJiVgif0,634
9
+ cycls/web.py,sha256=QCIr5YZRzwNFG2RrDVd4nsd3VwNr0_rFXQaX52ahTTo,5789
10
+ cycls-0.0.2.64.dist-info/METADATA,sha256=E_diO2pJp_US0Ziw8RQb-c_GBg9CxFjDXxv1Xgt-LCY,4843
11
+ cycls-0.0.2.64.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
+ cycls-0.0.2.64.dist-info/RECORD,,