cycls 0.0.2.24__py3-none-any.whl → 0.0.2.30__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 +1 -1
- cycls/cycls.py +158 -123
- cycls/theme/assets/index-D0-uI8sw.js +364 -0
- cycls/theme/index.html +23 -0
- {cycls-0.0.2.24.dist-info → cycls-0.0.2.30.dist-info}/METADATA +7 -6
- cycls-0.0.2.30.dist-info/RECORD +7 -0
- {cycls-0.0.2.24.dist-info → cycls-0.0.2.30.dist-info}/WHEEL +1 -1
- cycls/tuns +0 -27
- cycls-0.0.2.24.dist-info/RECORD +0 -6
cycls/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .cycls import
|
|
1
|
+
from .cycls import Agent
|
cycls/cycls.py
CHANGED
|
@@ -1,127 +1,162 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def sync_wrapper(*args, **kwargs):
|
|
80
|
-
return StreamingResponse(func(*args, **kwargs))
|
|
81
|
-
wrapper = async_wrapper if inspect.iscoroutinefunction(func) else sync_wrapper
|
|
82
|
-
self.apps[handle] = wrapper
|
|
83
|
-
return wrapper
|
|
84
|
-
return decorator
|
|
1
|
+
import json, time, modal, inspect, uvicorn
|
|
2
|
+
from modal.runner import run_app
|
|
3
|
+
|
|
4
|
+
import importlib.resources
|
|
5
|
+
theme_path = importlib.resources.files('cycls').joinpath('theme')
|
|
6
|
+
|
|
7
|
+
async def openai_encoder(stream): # clean up the meta data / new API?
|
|
8
|
+
async for message in stream:
|
|
9
|
+
payload = {"id": "chatcmpl-123",
|
|
10
|
+
"object": "chat.completion.chunk",
|
|
11
|
+
"created": 1728083325,
|
|
12
|
+
"model": "model-1-2025-01-01",
|
|
13
|
+
"system_fingerprint": "fp_123456",
|
|
14
|
+
"choices": [{"delta": {"content": message}}]}
|
|
15
|
+
if message:
|
|
16
|
+
yield f"data: {json.dumps(payload)}\n\n"
|
|
17
|
+
yield "data: [DONE]\n\n"
|
|
18
|
+
|
|
19
|
+
test_auth_public_key = """
|
|
20
|
+
-----BEGIN PUBLIC KEY-----
|
|
21
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyDudrDtQ5irw6hPWf2rw
|
|
22
|
+
FvNAFWeOouOO3XNWVQrjXCZfegiLYkL4cJdm4eqIuMdFHGnXU+gWT5P0EkLIkbtE
|
|
23
|
+
zpqDb5Wp27WpSRb5lqJehpU7FE+oQuovCwR9m5gYXP5rfM+CQ7ZPw/CcOQPtOB5G
|
|
24
|
+
0UijBhmYqws3SFp1Rk1uFed1F/esspt6Ifq2uDSHESleylqTKUCQiBa++z4wllcV
|
|
25
|
+
PbNiooLRpsF0kGljP2dXXy/ViF7q9Cblgl+FdrqtGfHD+DHJuOSYcPnRa0IHZYS4
|
|
26
|
+
r5i9C2lejVrEDqgJk5IbmQgez0wmEG4ynAxiDLvfdtvrd27PyBI75FsyLER/ydBH
|
|
27
|
+
WwIDAQAB
|
|
28
|
+
-----END PUBLIC KEY-----
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
live_auth_public_key = """
|
|
32
|
+
-----BEGIN PUBLIC KEY-----
|
|
33
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAorfL7XyxrLG/X+Kq9ImY
|
|
34
|
+
oSQ+Y3PY5qi8t8R4urY9u4ADJ48j9LkmFz8ALbubQkl3IByDDuVbka49m8id9isy
|
|
35
|
+
F9ZJErsZzzlYztrgI5Sg4R6OJXcNWLqh/tzutMWJFOrE3LnHXpeyQMo/6qAd59Dx
|
|
36
|
+
sNqzGxBTGPV1BZvpfhp/TT/sjgbPQWHS4PMpKD4vZLKXeTNJ913fMTUoFAIaL0sT
|
|
37
|
+
EhoeLUwvIuhLx4UYTmjO/sa+fS6mdghjddOkjSS/AWr/K8mN3IXDImGqh83L7/P0
|
|
38
|
+
RCru4Hvarm0qPIhfwEFfWhKFXONMj3x2fT4MM1Uw1H7qKTER2MtOjmdchKNX7x9b
|
|
39
|
+
XwIDAQAB
|
|
40
|
+
-----END PUBLIC KEY-----
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def web(func, front_end_path="", prod=False, org=None, api_token=None, header="", intro="", auth=True): # API auth
|
|
44
|
+
print(front_end_path)
|
|
45
|
+
from fastapi import FastAPI, Request, HTTPException, status, Depends
|
|
46
|
+
from fastapi.responses import StreamingResponse , HTMLResponse
|
|
47
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
48
|
+
import jwt
|
|
49
|
+
from pydantic import BaseModel, EmailStr
|
|
50
|
+
from typing import List, Optional
|
|
51
|
+
from fastapi.templating import Jinja2Templates
|
|
52
|
+
from fastapi.staticfiles import StaticFiles
|
|
53
|
+
|
|
54
|
+
class User(BaseModel):
|
|
55
|
+
id: str
|
|
56
|
+
name: str
|
|
57
|
+
email: EmailStr
|
|
58
|
+
org: Optional[str] = None
|
|
59
|
+
plans: List[str] = []
|
|
60
|
+
|
|
61
|
+
class Context(BaseModel):
|
|
62
|
+
messages: List[dict]
|
|
63
|
+
user: Optional[User] = None
|
|
64
|
+
|
|
65
|
+
app = FastAPI()
|
|
66
|
+
bearer_scheme = HTTPBearer()
|
|
67
|
+
|
|
68
|
+
def validate(bearer: HTTPAuthorizationCredentials = Depends(bearer_scheme)):
|
|
69
|
+
# if api_token and api_token==""
|
|
70
|
+
try:
|
|
71
|
+
public_key = live_auth_public_key if prod else test_auth_public_key
|
|
72
|
+
decoded = jwt.decode(bearer.credentials, public_key, algorithms=["RS256"])
|
|
73
|
+
# print(decoded)
|
|
74
|
+
return {"type": "user",
|
|
75
|
+
"user": {"id": decoded.get("id"), "name": decoded.get("name"), "email": decoded.get("email"), "org": decoded.get("org"),
|
|
76
|
+
"plans": decoded.get("public").get("plans", [])}}
|
|
77
|
+
except:
|
|
78
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
@app.post("/")
|
|
81
|
+
@app.post("/chat/completions")
|
|
82
|
+
async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
|
|
87
83
|
data = await request.json()
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
84
|
+
messages = data.get("messages")
|
|
85
|
+
user_data = jwt.get("user") if jwt else None
|
|
86
|
+
context = Context(messages = messages, user = User(**user_data) if user_data else None)
|
|
87
|
+
stream = await func(context) if inspect.iscoroutinefunction(func) else func(context)
|
|
88
|
+
if request.url.path == "/chat/completions":
|
|
89
|
+
stream = openai_encoder(stream)
|
|
90
|
+
return StreamingResponse(stream, media_type="text/event-stream")
|
|
91
|
+
|
|
92
|
+
templates = Jinja2Templates(directory=front_end_path)
|
|
93
|
+
@app.get("/", response_class=HTMLResponse)
|
|
94
|
+
async def front(request: Request):
|
|
95
|
+
return templates.TemplateResponse("index.html", {
|
|
96
|
+
"request": request, "header": header, "intro": intro, "prod": prod, "auth": auth, "org": org,
|
|
97
|
+
"pk_live": "pk_live_Y2xlcmsuY3ljbHMuY29tJA", "pk_test": "pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
|
|
98
|
+
})
|
|
99
|
+
app.mount("/", StaticFiles(directory=front_end_path, html=False))
|
|
100
|
+
return app
|
|
101
|
+
|
|
102
|
+
class Agent:
|
|
103
|
+
def __init__(self, front_end=theme_path, organization=None, api_token=None, pip=[], apt=[], copy=[], production=False, keys=["",""]):
|
|
104
|
+
self.prod, self.org, self.api_token = production, organization, api_token
|
|
105
|
+
self.front_end = front_end
|
|
106
|
+
self.registered_functions = []
|
|
107
|
+
self.client = modal.Client.from_credentials(*keys)
|
|
108
|
+
image = (modal.Image.debian_slim()
|
|
109
|
+
.pip_install("fastapi[standard]", "pyjwt", "cryptography", *pip)
|
|
110
|
+
.apt_install(*apt)
|
|
111
|
+
.add_local_dir(front_end, "/root/public")
|
|
112
|
+
.add_local_python_source("cycls"))
|
|
113
|
+
for item in copy:
|
|
114
|
+
image = image.add_local_file(item, f"/root/{item}") if "." in item else image.add_local_dir(item, f'/root/{item}')
|
|
115
|
+
self.app = modal.App("development", image=image)
|
|
116
|
+
|
|
117
|
+
def __call__(self, name="", header="", intro="", domain=None, auth=False):
|
|
118
|
+
def decorator(f):
|
|
119
|
+
self.registered_functions.append({
|
|
120
|
+
"func": f,
|
|
121
|
+
"config": ["public", self.prod, self.org, self.api_token, header, intro, auth],
|
|
122
|
+
"name": name,
|
|
123
|
+
"domain": domain or f"{name}.cycls.ai",
|
|
124
|
+
})
|
|
125
|
+
return f
|
|
126
|
+
return decorator
|
|
127
|
+
|
|
128
|
+
def run(self, port=8000):
|
|
129
|
+
if not self.registered_functions:
|
|
130
|
+
return print("Error: No @agent decorated function found.")
|
|
126
131
|
|
|
132
|
+
i = self.registered_functions[0]
|
|
133
|
+
if len(self.registered_functions) > 1:
|
|
134
|
+
print(f"⚠️ Warning: Multiple agents found. Running '{i['name']}'.")
|
|
135
|
+
print(f"🚀 Starting local server at http://127.0.0.1:{port}")
|
|
136
|
+
i["config"][0] = self.front_end
|
|
137
|
+
uvicorn.run(web(i["func"], *i["config"]), host="127.0.0.1", port=port)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
def push(self): # local / prod?
|
|
141
|
+
if not self.registered_functions:
|
|
142
|
+
return print("Error: No @agent decorated function found.")
|
|
143
|
+
|
|
144
|
+
for i in self.registered_functions:
|
|
145
|
+
self.app.function(serialized=True, name=i["name"])(
|
|
146
|
+
modal.asgi_app(label=i["name"], custom_domains=[i["domain"]])
|
|
147
|
+
(lambda: web(i["func"], *i["config"]))
|
|
148
|
+
)
|
|
149
|
+
if self.prod:
|
|
150
|
+
for i in self.registered_functions:
|
|
151
|
+
print(f"✅ Deployed to ⇒ https://{i['domain']}")
|
|
152
|
+
self.app.deploy(client=self.client, name=self.registered_functions[0]["name"])
|
|
153
|
+
return
|
|
154
|
+
else:
|
|
155
|
+
with modal.enable_output():
|
|
156
|
+
run_app(app=self.app, client=self.client)
|
|
157
|
+
print(" Modal development server is running. Press Ctrl+C to stop.")
|
|
158
|
+
with modal.enable_output(), run_app(app=self.app, client=self.client):
|
|
159
|
+
while True: time.sleep(10)
|
|
160
|
+
|
|
161
|
+
# poetry run python agent.py
|
|
127
162
|
# poetry publish --build
|