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.
- cycls/__init__.py +2 -1
- cycls/{theme/assets/index-DWGS8zpa.css → default-theme/assets/index-B0ZKcm_V.css} +1 -1
- cycls/default-theme/assets/index-D5EDcI4J.js +422 -0
- cycls/{theme → default-theme}/index.html +3 -3
- cycls/dev-theme/index.html +305 -0
- cycls/sdk.py +16 -11
- cycls/ui.py +6 -0
- cycls/web.py +83 -56
- {cycls-0.0.2.62.dist-info → cycls-0.0.2.64.dist-info}/METADATA +4 -3
- cycls-0.0.2.64.dist-info/RECORD +12 -0
- cycls/theme/assets/index-qVg4Gbap.js +0 -419
- cycls-0.0.2.62.dist-info/RECORD +0 -10
- {cycls-0.0.2.62.dist-info → cycls-0.0.2.64.dist-info}/WHEEL +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!
|
|
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-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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:
|
|
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
|
-
|
|
91
|
-
decoded = jwt.decode(bearer.credentials,
|
|
92
|
-
|
|
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 =
|
|
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=
|
|
123
|
-
pk_test=
|
|
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.
|
|
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:
|
|
20
|
-
Requires-Dist:
|
|
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,,
|