cycls 0.0.2.70__py3-none-any.whl → 0.0.2.72__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/cli.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
# ANSI codes
|
|
8
|
+
DIM = "\033[2m"
|
|
9
|
+
BOLD = "\033[1m"
|
|
10
|
+
RESET = "\033[0m"
|
|
11
|
+
CLEAR_LINE = "\r\033[K"
|
|
12
|
+
GREEN = "\033[32m"
|
|
13
|
+
YELLOW = "\033[33m"
|
|
14
|
+
BLUE = "\033[34m"
|
|
15
|
+
RED = "\033[31m"
|
|
16
|
+
CYAN = "\033[36m"
|
|
17
|
+
MAGENTA = "\033[35m"
|
|
18
|
+
|
|
19
|
+
CALLOUT_STYLES = {
|
|
20
|
+
"success": ("✓", GREEN),
|
|
21
|
+
"warning": ("⚠", YELLOW),
|
|
22
|
+
"info": ("ℹ", BLUE),
|
|
23
|
+
"error": ("✗", RED),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def format_time(seconds):
|
|
27
|
+
if seconds < 60:
|
|
28
|
+
return f"{seconds:.1f}s"
|
|
29
|
+
return f"{int(seconds // 60)}m {int(seconds % 60)}s"
|
|
30
|
+
|
|
31
|
+
def render_table(headers, rows):
|
|
32
|
+
if not headers:
|
|
33
|
+
return
|
|
34
|
+
widths = [len(str(h)) for h in headers]
|
|
35
|
+
for row in rows:
|
|
36
|
+
for i, cell in enumerate(row):
|
|
37
|
+
if i < len(widths):
|
|
38
|
+
widths[i] = max(widths[i], len(str(cell)))
|
|
39
|
+
|
|
40
|
+
top = "┌" + "┬".join("─" * (w + 2) for w in widths) + "┐"
|
|
41
|
+
sep = "├" + "┼".join("─" * (w + 2) for w in widths) + "┤"
|
|
42
|
+
bot = "└" + "┴".join("─" * (w + 2) for w in widths) + "┘"
|
|
43
|
+
|
|
44
|
+
def fmt_row(cells, bold=False):
|
|
45
|
+
parts = []
|
|
46
|
+
for i, w in enumerate(widths):
|
|
47
|
+
cell = str(cells[i]) if i < len(cells) else ""
|
|
48
|
+
if bold:
|
|
49
|
+
parts.append(f" {BOLD}{cell.ljust(w)}{RESET} ")
|
|
50
|
+
else:
|
|
51
|
+
parts.append(f" {cell.ljust(w)} ")
|
|
52
|
+
return "│" + "│".join(parts) + "│"
|
|
53
|
+
|
|
54
|
+
print(top)
|
|
55
|
+
print(fmt_row(headers, bold=True))
|
|
56
|
+
print(sep)
|
|
57
|
+
for row in rows:
|
|
58
|
+
print(fmt_row(row))
|
|
59
|
+
print(bot)
|
|
60
|
+
|
|
61
|
+
class Spinner:
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.active = False
|
|
64
|
+
self.thread = None
|
|
65
|
+
self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
66
|
+
|
|
67
|
+
def start(self):
|
|
68
|
+
self.active = True
|
|
69
|
+
def spin():
|
|
70
|
+
i = 0
|
|
71
|
+
while self.active:
|
|
72
|
+
if not self.active:
|
|
73
|
+
break
|
|
74
|
+
print(f"\r{MAGENTA}{self.frames[i % len(self.frames)]}{RESET} ", end="", flush=True)
|
|
75
|
+
for _ in range(8): # Check active more frequently
|
|
76
|
+
if not self.active:
|
|
77
|
+
break
|
|
78
|
+
time.sleep(0.01)
|
|
79
|
+
i += 1
|
|
80
|
+
self.thread = threading.Thread(target=spin, daemon=True)
|
|
81
|
+
self.thread.start()
|
|
82
|
+
|
|
83
|
+
def stop(self):
|
|
84
|
+
self.active = False
|
|
85
|
+
if self.thread:
|
|
86
|
+
self.thread.join(timeout=0.2)
|
|
87
|
+
print(f"{CLEAR_LINE}", end="", flush=True)
|
|
88
|
+
|
|
89
|
+
def chat(url):
|
|
90
|
+
messages = []
|
|
91
|
+
endpoint = f"{url.rstrip('/')}/chat/cycls"
|
|
92
|
+
|
|
93
|
+
print(f"\n{MAGENTA}●{RESET} {BOLD}{url}{RESET}\n")
|
|
94
|
+
|
|
95
|
+
while True:
|
|
96
|
+
try:
|
|
97
|
+
user_input = input(f"{CYAN}❯{RESET} ")
|
|
98
|
+
if not user_input.strip():
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
messages.append({"role": "user", "content": user_input})
|
|
102
|
+
print()
|
|
103
|
+
|
|
104
|
+
start_time = time.time()
|
|
105
|
+
in_thinking = False
|
|
106
|
+
table_headers = []
|
|
107
|
+
table_rows = []
|
|
108
|
+
|
|
109
|
+
with httpx.stream("POST", endpoint, json={"messages": messages}, timeout=None) as r:
|
|
110
|
+
for line in r.iter_lines():
|
|
111
|
+
if line.startswith("data: ") and line != "data: [DONE]":
|
|
112
|
+
|
|
113
|
+
data = json.loads(line[6:])
|
|
114
|
+
msg_type = data.get("type")
|
|
115
|
+
|
|
116
|
+
# Handle plain string or missing type
|
|
117
|
+
if msg_type is None:
|
|
118
|
+
# Could be OpenAI format or plain text
|
|
119
|
+
if isinstance(data, str):
|
|
120
|
+
print(data, end="", flush=True)
|
|
121
|
+
continue
|
|
122
|
+
elif "choices" in data:
|
|
123
|
+
# OpenAI format
|
|
124
|
+
content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
|
125
|
+
if content:
|
|
126
|
+
print(content, end="", flush=True)
|
|
127
|
+
continue
|
|
128
|
+
elif "text" in data:
|
|
129
|
+
print(data.get("text", ""), end="", flush=True)
|
|
130
|
+
continue
|
|
131
|
+
elif "content" in data:
|
|
132
|
+
print(data.get("content", ""), end="", flush=True)
|
|
133
|
+
continue
|
|
134
|
+
else:
|
|
135
|
+
# Debug: print raw data
|
|
136
|
+
print(f"[debug: {data}]", end="", flush=True)
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Close thinking if switching
|
|
140
|
+
if msg_type != "thinking" and in_thinking:
|
|
141
|
+
print(f"</thinking>{RESET}\n", end="", flush=True)
|
|
142
|
+
in_thinking = False
|
|
143
|
+
|
|
144
|
+
# Flush table if switching
|
|
145
|
+
if msg_type != "table" and table_headers:
|
|
146
|
+
render_table(table_headers, table_rows)
|
|
147
|
+
table_headers = []
|
|
148
|
+
table_rows = []
|
|
149
|
+
|
|
150
|
+
if msg_type == "thinking":
|
|
151
|
+
if not in_thinking:
|
|
152
|
+
print(f"{DIM}<thinking>", end="", flush=True)
|
|
153
|
+
in_thinking = True
|
|
154
|
+
print(data.get("thinking", ""), end="", flush=True)
|
|
155
|
+
|
|
156
|
+
elif msg_type == "text":
|
|
157
|
+
print(data.get("text", ""), end="", flush=True)
|
|
158
|
+
|
|
159
|
+
elif msg_type == "code":
|
|
160
|
+
lang = data.get("language", "")
|
|
161
|
+
print(f"\n```{lang}\n{data.get('code', '')}\n```\n", end="", flush=True)
|
|
162
|
+
|
|
163
|
+
elif msg_type == "status":
|
|
164
|
+
print(f"{DIM}[{data.get('status', '')}]{RESET} ", end="", flush=True)
|
|
165
|
+
|
|
166
|
+
elif msg_type == "table":
|
|
167
|
+
if "headers" in data:
|
|
168
|
+
if table_headers:
|
|
169
|
+
render_table(table_headers, table_rows)
|
|
170
|
+
table_headers = data["headers"]
|
|
171
|
+
table_rows = []
|
|
172
|
+
elif "row" in data:
|
|
173
|
+
table_rows.append(data["row"])
|
|
174
|
+
|
|
175
|
+
elif msg_type == "callout":
|
|
176
|
+
style = data.get("style", "info")
|
|
177
|
+
icon, color = CALLOUT_STYLES.get(style, ("•", RESET))
|
|
178
|
+
title = data.get("title", "")
|
|
179
|
+
text = data.get("callout", "")
|
|
180
|
+
if title:
|
|
181
|
+
print(f"\n{color}{icon} {BOLD}{title}{RESET}")
|
|
182
|
+
print(f"{color} {text}{RESET}\n", end="", flush=True)
|
|
183
|
+
else:
|
|
184
|
+
print(f"\n{color}{icon} {text}{RESET}\n", end="", flush=True)
|
|
185
|
+
|
|
186
|
+
elif msg_type == "image":
|
|
187
|
+
print(f"{DIM}[image: {data.get('src', '')}]{RESET}", end="", flush=True)
|
|
188
|
+
|
|
189
|
+
# Flush remaining
|
|
190
|
+
if table_headers:
|
|
191
|
+
render_table(table_headers, table_rows)
|
|
192
|
+
if in_thinking:
|
|
193
|
+
print(f"</thinking>{RESET}", end="", flush=True)
|
|
194
|
+
|
|
195
|
+
elapsed = time.time() - start_time
|
|
196
|
+
print(f"\n\n{DIM}✦ {format_time(elapsed)}{RESET}\n")
|
|
197
|
+
|
|
198
|
+
except KeyboardInterrupt:
|
|
199
|
+
continue
|
|
200
|
+
except EOFError:
|
|
201
|
+
print(f"{RESET}\n👋")
|
|
202
|
+
break
|
|
203
|
+
except (httpx.ReadError, httpx.ConnectError):
|
|
204
|
+
print(f"{RESET}🔄 Reconnecting...", end="", flush=True)
|
|
205
|
+
time.sleep(1)
|
|
206
|
+
print(CLEAR_LINE, end="")
|
|
207
|
+
if messages:
|
|
208
|
+
messages.pop()
|
|
209
|
+
|
|
210
|
+
def main():
|
|
211
|
+
if len(sys.argv) < 3 or sys.argv[1] != "chat":
|
|
212
|
+
print("Usage: cycls chat <url>")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
chat(sys.argv[2])
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
main()
|
cycls/runtime.py
CHANGED
|
@@ -178,9 +178,11 @@ class Runtime:
|
|
|
178
178
|
|
|
179
179
|
def _generate_dockerfile(self, port=None) -> str:
|
|
180
180
|
"""Generates a multi-stage Dockerfile string."""
|
|
181
|
-
|
|
181
|
+
using_base = self.base_image == BASE_IMAGE
|
|
182
|
+
|
|
183
|
+
# Only install extra packages not in base image (use uv if available in base)
|
|
182
184
|
run_pip_install = (
|
|
183
|
-
f"RUN pip install --no-cache
|
|
185
|
+
f"RUN uv pip install --system --no-cache {' '.join(self.pip_packages)}"
|
|
184
186
|
if self.pip_packages else ""
|
|
185
187
|
)
|
|
186
188
|
run_apt_install = (
|
|
@@ -191,16 +193,19 @@ class Runtime:
|
|
|
191
193
|
copy_lines = "\n".join([f"COPY context_files/{dst} {dst}" for dst in self.copy.values()])
|
|
192
194
|
expose_line = f"EXPOSE {port}" if port else ""
|
|
193
195
|
|
|
196
|
+
# Skip env/mkdir/workdir if using pre-built base (already configured)
|
|
197
|
+
env_lines = "" if using_base else f"""ENV PIP_ROOT_USER_ACTION=ignore \\
|
|
198
|
+
PYTHONUNBUFFERED=1
|
|
199
|
+
RUN mkdir -p {self.io_dir}
|
|
200
|
+
WORKDIR /app"""
|
|
201
|
+
|
|
194
202
|
return f"""
|
|
195
203
|
# STAGE 1: Base image with all dependencies
|
|
196
204
|
FROM {self.base_image} as base
|
|
197
|
-
|
|
198
|
-
ENV PYTHONUNBUFFERED=1
|
|
199
|
-
RUN mkdir -p {self.io_dir}
|
|
205
|
+
{env_lines}
|
|
200
206
|
{run_apt_install}
|
|
201
207
|
{run_pip_install}
|
|
202
208
|
{run_shell_commands}
|
|
203
|
-
WORKDIR /app
|
|
204
209
|
{copy_lines}
|
|
205
210
|
COPY {self.runner_filename} {self.runner_path}
|
|
206
211
|
ENTRYPOINT ["python", "{self.runner_path}", "{self.io_dir}"]
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
cycls/__init__.py,sha256=bVT0dYTXLdSC3ZURgtm-DEOj-VO6RUM6zGsJB0zuj6Y,61
|
|
2
2
|
cycls/auth.py,sha256=xkndHZyCfnlertMMEKerCJjf23N3fVcTRVTTSXTTuzg,247
|
|
3
|
+
cycls/cli.py,sha256=AKf0z7ZLau3GvBVR_IhB7agmq4nVaHkcuUafNyvv2_A,7978
|
|
3
4
|
cycls/default-theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlFbMwyC9WqTmSKUaKw,6546
|
|
4
5
|
cycls/default-theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
|
|
5
6
|
cycls/default-theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
|
|
6
7
|
cycls/dev-theme/index.html,sha256=QJBHkdNuMMiwQU7o8dN8__8YQeQB45D37D-NCXIWB2Q,11585
|
|
7
|
-
cycls/runtime.py,sha256=
|
|
8
|
+
cycls/runtime.py,sha256=lg7XKHd9fLV_bYksHv2LHf3Lq7HPAC3K5Tr8pNgQ7sM,21641
|
|
8
9
|
cycls/sdk.py,sha256=6oRKP44TJN9HKdNw9OYzDlZFDUMUhoCMt8TEwyu26dI,7368
|
|
9
10
|
cycls/web.py,sha256=3M3qaWTNY3dpgd7Vq5aXREp-cIFsHrDqBQ1YkGrOaUk,4659
|
|
10
|
-
cycls-0.0.2.
|
|
11
|
-
cycls-0.0.2.
|
|
12
|
-
cycls-0.0.2.
|
|
11
|
+
cycls-0.0.2.72.dist-info/METADATA,sha256=8co1QLXI-88tAFHSuaWeZchypHvODZVC1Qjlo9A_WDE,8008
|
|
12
|
+
cycls-0.0.2.72.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
13
|
+
cycls-0.0.2.72.dist-info/entry_points.txt,sha256=vEhqUxFhhuzCKWtq02LbMnT3wpUqdfgcM3Yh-jjXom8,40
|
|
14
|
+
cycls-0.0.2.72.dist-info/RECORD,,
|
|
File without changes
|