cycls 0.0.2.79__tar.gz → 0.0.2.80__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.
- {cycls-0.0.2.79 → cycls-0.0.2.80}/PKG-INFO +3 -1
- {cycls-0.0.2.79 → cycls-0.0.2.80}/README.md +2 -0
- cycls-0.0.2.80/cycls/chat.py +95 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/pyproject.toml +2 -2
- cycls-0.0.2.79/cycls/cli.py +0 -217
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/__init__.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/auth.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/default-theme/assets/index-B0ZKcm_V.css +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/default-theme/assets/index-D5EDcI4J.js +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/default-theme/index.html +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/dev-theme/index.html +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/grpc/__init__.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/grpc/client.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/grpc/runtime.proto +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/grpc/runtime_pb2.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/grpc/runtime_pb2_grpc.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/grpc/server.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/runtime.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/sdk.py +0 -0
- {cycls-0.0.2.79 → cycls-0.0.2.80}/cycls/web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cycls
|
|
3
|
-
Version: 0.0.2.
|
|
3
|
+
Version: 0.0.2.80
|
|
4
4
|
Summary: Distribute Intelligence
|
|
5
5
|
Author: Mohammed J. AlRujayi
|
|
6
6
|
Author-email: mj@cycls.com
|
|
@@ -47,6 +47,8 @@ Distribute Intelligence
|
|
|
47
47
|
|
|
48
48
|
The open-source SDK for distributing AI agents.
|
|
49
49
|
|
|
50
|
+
The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
|
|
51
|
+
|
|
50
52
|
## Distribute Intelligence
|
|
51
53
|
|
|
52
54
|
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
@@ -22,6 +22,8 @@ Distribute Intelligence
|
|
|
22
22
|
|
|
23
23
|
The open-source SDK for distributing AI agents.
|
|
24
24
|
|
|
25
|
+
The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
|
|
26
|
+
|
|
25
27
|
## Distribute Intelligence
|
|
26
28
|
|
|
27
29
|
Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cycls chat - Claude Code style CLI for cycls agents"""
|
|
3
|
+
|
|
4
|
+
import json, os, re, sys
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
|
|
8
|
+
BLUE, GREEN, YELLOW, RED = "\033[34m", "\033[32m", "\033[33m", "\033[31m"
|
|
9
|
+
CALLOUTS = {"success": ("✓", GREEN), "warning": ("⚠", YELLOW), "info": ("ℹ", BLUE), "error": ("✗", RED)}
|
|
10
|
+
|
|
11
|
+
separator = lambda: f"{DIM}{'─' * min(os.get_terminal_size().columns if sys.stdout.isatty() else 80, 80)}{RESET}"
|
|
12
|
+
markdown = lambda text: re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
|
|
13
|
+
header = lambda title, meta, color=GREEN, dim=False: print(f"{color}●{RESET} {BOLD}{title}{RESET}\n ⎿ {meta}{DIM if dim else ''}", flush=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def table(headers, rows):
|
|
17
|
+
if not headers: return
|
|
18
|
+
widths = [max(len(str(h)), *(len(str(r[i])) for r in rows if i < len(r))) for i, h in enumerate(headers)]
|
|
19
|
+
line = lambda left, mid, right: left + mid.join("─" * (w + 2) for w in widths) + right
|
|
20
|
+
row = lambda cells, bold=False: "│" + "│".join(f" {BOLD if bold else ''}{str(cells[i] if i < len(cells) else '').ljust(widths[i])}{RESET if bold else ''} " for i in range(len(widths))) + "│"
|
|
21
|
+
print(f"{line('┌', '┬', '┐')}\n{row(headers, True)}\n{line('├', '┼', '┤')}")
|
|
22
|
+
for r in rows: print(row(r))
|
|
23
|
+
print(line("└", "┴", "┘"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def chat(url):
|
|
27
|
+
messages, endpoint = [], f"{url.rstrip('/')}/chat/cycls"
|
|
28
|
+
print(f"\n{BOLD}cycls{RESET} {DIM}|{RESET} {url}\n")
|
|
29
|
+
|
|
30
|
+
while True:
|
|
31
|
+
try:
|
|
32
|
+
print(separator())
|
|
33
|
+
user_input = input(f"{BOLD}{BLUE}❯{RESET} ").strip()
|
|
34
|
+
print(separator())
|
|
35
|
+
|
|
36
|
+
if not user_input: continue
|
|
37
|
+
if user_input in ("/q", "exit", "quit"): break
|
|
38
|
+
if user_input == "/c": messages, _ = [], print(f"{GREEN}⏺ Cleared{RESET}"); continue
|
|
39
|
+
|
|
40
|
+
messages.append({"role": "user", "content": user_input})
|
|
41
|
+
block, tbl = None, ([], [])
|
|
42
|
+
|
|
43
|
+
def close():
|
|
44
|
+
nonlocal block, tbl
|
|
45
|
+
if block == "thinking": print(RESET)
|
|
46
|
+
if block == "text": print()
|
|
47
|
+
if block == "table" and tbl[0]: table(*tbl); tbl = ([], [])
|
|
48
|
+
if block: print()
|
|
49
|
+
block = None
|
|
50
|
+
|
|
51
|
+
with httpx.stream("POST", endpoint, json={"messages": messages}, timeout=None) as response:
|
|
52
|
+
for line in response.iter_lines():
|
|
53
|
+
if not line.startswith("data: ") or line == "data: [DONE]": continue
|
|
54
|
+
data = json.loads(line[6:])
|
|
55
|
+
type = data.get("type")
|
|
56
|
+
|
|
57
|
+
if type is None:
|
|
58
|
+
print(markdown(data if isinstance(data, str) else data.get("text", "")), end="", flush=True); continue
|
|
59
|
+
|
|
60
|
+
if type != block: close()
|
|
61
|
+
|
|
62
|
+
if type in ("thinking", "text"):
|
|
63
|
+
if block != type: header(type.capitalize(), "Live", dim=(type == "thinking")); block = type
|
|
64
|
+
print((markdown if type == "text" else str)(data.get(type, "")), end="", flush=True)
|
|
65
|
+
elif type == "code":
|
|
66
|
+
code = data.get("code", ""); header(f"Code({data.get('language', '')})", f"{code.count(chr(10))+1} lines"); print(code, flush=True); block = type
|
|
67
|
+
elif type == "status":
|
|
68
|
+
print(f"{DIM}[{data.get('status', '')}]{RESET} ", end="", flush=True)
|
|
69
|
+
elif type == "table":
|
|
70
|
+
if "headers" in data:
|
|
71
|
+
if tbl[0]: table(*tbl)
|
|
72
|
+
header("Table", f"{len(data['headers'])} cols"); tbl, block = (data["headers"], []), type
|
|
73
|
+
elif "row" in data: tbl[1].append(data["row"])
|
|
74
|
+
elif type == "callout":
|
|
75
|
+
style = data.get("style", "info"); icon, color = CALLOUTS.get(style, ("•", RESET))
|
|
76
|
+
header(style.capitalize(), f"{icon} {data.get('callout', '')}", color=color); block = type
|
|
77
|
+
elif type == "image":
|
|
78
|
+
header("Image", data.get("src", "")); block = type
|
|
79
|
+
|
|
80
|
+
close()
|
|
81
|
+
|
|
82
|
+
except KeyboardInterrupt: print()
|
|
83
|
+
except EOFError: break
|
|
84
|
+
except (httpx.ReadError, httpx.ConnectError) as e: print(f"{RED}⏺ Connection error: {e}{RESET}"); messages and messages.pop()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
len(sys.argv) < 2 and (print("Usage: cycls chat <url|port>"), sys.exit(1))
|
|
89
|
+
arg = sys.argv[1]
|
|
90
|
+
url = f"http://localhost:{arg}" if arg.isdigit() else arg
|
|
91
|
+
chat(url)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cycls"
|
|
3
|
-
version = "0.0.2.
|
|
3
|
+
version = "0.0.2.80"
|
|
4
4
|
|
|
5
5
|
packages = [{ include = "cycls" }]
|
|
6
6
|
include = ["cycls/theme/**/*"]
|
|
@@ -19,7 +19,7 @@ grpcio = "^1.76.0"
|
|
|
19
19
|
protobuf = "^6.0"
|
|
20
20
|
|
|
21
21
|
[tool.poetry.scripts]
|
|
22
|
-
cycls = "cycls.
|
|
22
|
+
cycls = "cycls.chat:main"
|
|
23
23
|
|
|
24
24
|
[tool.poetry.group.test.dependencies]
|
|
25
25
|
pytest = "^8.0.0"
|
cycls-0.0.2.79/cycls/cli.py
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|