cycls 0.0.2.63__tar.gz → 0.0.2.64__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.63
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,3 @@
1
+ from .sdk import Agent, function
2
+ from .runtime import Runtime
3
+ from . import ui as UI
@@ -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>
@@ -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],
@@ -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}
@@ -0,0 +1,157 @@
1
+ import json, inspect
2
+ from pathlib import Path
3
+
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"
16
+ yield "data: [DONE]\n\n"
17
+
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
48
+ yield "data: [DONE]\n\n"
49
+
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
68
+
69
+ def web(func, public_path="", prod=False, org=None, api_token=None, header="", intro="", title="", auth=False, tier="", analytics=False): # API auth
70
+ from fastapi import FastAPI, Request, HTTPException, status, Depends
71
+ from fastapi.responses import StreamingResponse , HTMLResponse
72
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
73
+ import jwt
74
+ from jwt import PyJWKClient
75
+ from pydantic import BaseModel, EmailStr
76
+ from typing import List, Optional, Any
77
+ from fastapi.staticfiles import StaticFiles
78
+
79
+ jwks = PyJWKClient(JWKS_PROD if prod else JWKS_TEST)
80
+
81
+ class User(BaseModel):
82
+ id: str
83
+ name: Optional[str] = None
84
+ email: EmailStr
85
+ org: Optional[str] = None
86
+ plans: List[str] = []
87
+
88
+ class Metadata(BaseModel):
89
+ header: str
90
+ intro: str
91
+ title: str
92
+ prod: bool
93
+ auth: bool
94
+ tier: str
95
+ analytics: bool
96
+ org: Optional[str]
97
+ pk_live: str
98
+ pk_test: str
99
+
100
+ class Context(BaseModel):
101
+ messages: Any
102
+ user: Optional[User] = None
103
+
104
+ app = FastAPI()
105
+ bearer_scheme = HTTPBearer()
106
+
107
+ def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
108
+ try:
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",
112
+ "user": {"id": decoded.get("id"), "name": decoded.get("name"), "email": decoded.get("email"), "org": decoded.get("org"),
113
+ "plans": decoded.get("public", {}).get("plans", [])}}
114
+ except:
115
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
116
+
117
+ @app.post("/")
118
+ @app.post("/chat/cycls")
119
+ @app.post("/chat/completions")
120
+ async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
121
+ data = await request.json()
122
+ messages = data.get("messages")
123
+ user_data = jwt.get("user") if jwt else None
124
+ context = Context(messages = Messages(messages), user = User(**user_data) if user_data else None)
125
+ stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
126
+ if request.url.path == "/chat/completions":
127
+ stream = openai_encoder(stream)
128
+ elif request.url.path == "/chat/cycls":
129
+ stream = encoder(stream)
130
+ return StreamingResponse(stream, media_type="text/event-stream")
131
+
132
+ @app.get("/metadata")
133
+ async def metadata():
134
+ return Metadata(
135
+ header=header,
136
+ intro=intro,
137
+ title=title,
138
+ prod=prod,
139
+ auth=auth,
140
+ tier=tier,
141
+ analytics=analytics,
142
+ org=org,
143
+ pk_live=PK_LIVE,
144
+ pk_test=PK_TEST
145
+ )
146
+
147
+ if Path("public").is_dir():
148
+ app.mount("/public", StaticFiles(directory="public", html=True))
149
+ app.mount("/", StaticFiles(directory=public_path, html=True))
150
+
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
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.63"
3
+ version = "0.0.2.64"
4
4
 
5
5
  packages = [{ include = "cycls" }]
6
6
  include = ["cycls/theme/**/*"]
@@ -12,11 +12,17 @@ readme = "README.md"
12
12
  python = "^3.9"
13
13
  fastapi = "^0.111.0"
14
14
  httpx = "^0.27.0"
15
- modal = "^1.1.0"
16
- jwt = "^1.4.0"
15
+ pyjwt = "^2.8.0"
17
16
  docker = "^7.1.0"
18
17
  cloudpickle = "^3.1.1"
19
18
 
19
+ [tool.poetry.extras]
20
+ modal = ["modal"]
21
+
22
+ [tool.poetry.dependencies.modal]
23
+ version = "^1.1.0"
24
+ optional = true
25
+
20
26
  [build-system]
21
27
  requires = ["poetry-core"]
22
28
  build-backend = "poetry.core.masonry.api"
@@ -1,2 +0,0 @@
1
- from .sdk import Agent, function
2
- from .runtime import Runtime
@@ -1,130 +0,0 @@
1
- import json, inspect
2
- from pathlib import Path
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"
14
- yield "data: [DONE]\n\n"
15
-
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"
26
- yield "data: [DONE]\n\n"
27
-
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
- """
51
-
52
- def web(func, public_path="", prod=False, org=None, api_token=None, header="", intro="", title="", auth=False, tier="", analytics=False): # API auth
53
- from fastapi import FastAPI, Request, HTTPException, status, Depends
54
- from fastapi.responses import StreamingResponse , HTMLResponse
55
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
56
- import jwt
57
- from pydantic import BaseModel, EmailStr
58
- from typing import List, Optional
59
- from fastapi.staticfiles import StaticFiles
60
-
61
- class User(BaseModel):
62
- id: str
63
- name: Optional[str] = None
64
- email: EmailStr
65
- org: Optional[str] = None
66
- plans: List[str] = []
67
-
68
- class Metadata(BaseModel):
69
- header: str
70
- intro: str
71
- title: str
72
- prod: bool
73
- auth: bool
74
- tier: str
75
- analytics: bool
76
- org: Optional[str]
77
- pk_live: str
78
- pk_test: str
79
-
80
- class Context(BaseModel):
81
- messages: List[dict]
82
- user: Optional[User] = None
83
-
84
- app = FastAPI()
85
- bearer_scheme = HTTPBearer()
86
-
87
- def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
88
- # if api_token and api_token==""
89
- 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",
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", [])}}
96
- except:
97
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
98
-
99
- @app.post("/")
100
- @app.post("/chat/completions")
101
- async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
102
- data = await request.json()
103
- messages = data.get("messages")
104
- user_data = jwt.get("user") if jwt else None
105
- context = Context(messages = messages, user = User(**user_data) if user_data else None)
106
- stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
107
- if request.url.path == "/chat/completions":
108
- stream = async_openai_encoder(stream) if inspect.isasyncgen(stream) else openai_encoder(stream)
109
- return StreamingResponse(stream, media_type="text/event-stream")
110
-
111
- @app.get("/metadata")
112
- async def metadata():
113
- return Metadata(
114
- header=header,
115
- intro=intro,
116
- title=title,
117
- prod=prod,
118
- auth=auth,
119
- tier=tier,
120
- analytics=analytics,
121
- org=org,
122
- pk_live="pk_live_Y2xlcmsuY3ljbHMuYWkk",
123
- pk_test="pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
124
- )
125
-
126
- if Path("public").is_dir():
127
- app.mount("/public", StaticFiles(directory="public", html=True))
128
- app.mount("/", StaticFiles(directory=public_path, html=True))
129
-
130
- return app
File without changes
File without changes