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.
- cycls/dev-theme/index.html +298 -0
- cycls/sdk.py +16 -11
- cycls/web.py +65 -56
- cycls-0.0.2.65.dist-info/METADATA +269 -0
- cycls-0.0.2.65.dist-info/RECORD +11 -0
- cycls-0.0.2.63.dist-info/METADATA +0 -140
- cycls-0.0.2.63.dist-info/RECORD +0 -10
- /cycls/{theme → default-theme}/assets/index-B0ZKcm_V.css +0 -0
- /cycls/{theme → default-theme}/assets/index-D5EDcI4J.js +0 -0
- /cycls/{theme → default-theme}/index.html +0 -0
- {cycls-0.0.2.63.dist-info → cycls-0.0.2.65.dist-info}/WHEEL +0 -0
|
@@ -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=
|
|
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/web.py
CHANGED
|
@@ -1,63 +1,65 @@
|
|
|
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
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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:
|
|
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
|
-
|
|
91
|
-
decoded = jwt.decode(bearer.credentials,
|
|
92
|
-
|
|
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 =
|
|
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=
|
|
123
|
-
pk_test=
|
|
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.
|
cycls-0.0.2.63.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|