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 CHANGED
@@ -1 +1 @@
1
- from .cycls import Cycls
1
+ from .cycls import Agent
cycls/cycls.py CHANGED
@@ -1,127 +1,162 @@
1
- from fastapi import FastAPI, Request
2
- from fastapi.responses import StreamingResponse
3
- from pydantic import BaseModel
4
- from typing import List, Dict, Optional
5
-
6
- from functools import wraps
7
- import uvicorn, socket, httpx
8
- import inspect
9
-
10
- import logging
11
- logging.basicConfig(level=logging.ERROR)
12
-
13
- O = lambda x,y: print(f"✦/✧ {str(x).ljust(12)} | {y}")
14
-
15
- import os
16
- current_dir = os.path.dirname(os.path.abspath(__file__))
17
- key_path = os.path.join(current_dir, 'tuns')
18
-
19
- class Message(BaseModel):
20
- handle: str
21
- content: str
22
- id: str
23
- history: Optional[List[Dict[str, str]]] = None
24
-
25
- #!
26
- def find_available_port(start_port):
27
- port = start_port
28
- while True:
29
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
30
- if s.connect_ex(('localhost', port)) != 0:
31
- return port
32
- port += 1
33
-
34
- import asyncssh, asyncio
35
- # ssh -q -i tuns -o StrictHostKeyChecking=no -R 80:localhost:9000 tuns.sh
36
- async def create_ssh_tunnel(x,y,z='tuns.sh'):
37
- try:
38
- async with asyncssh.connect(z,client_keys=[key_path],known_hosts=None) as conn:
39
- listener = await conn.forward_remote_port(x, 80, 'localhost', y)
40
- O("tunnel","open");print(" ")
41
- await listener.wait_closed()
42
- O("tunnel", "closed")
43
- except (OSError, asyncssh.Error) as e:
44
- O("tunnel",f"disconnected ({e})")
45
-
46
- def register(handles, net, api_key):
47
- try:
48
- with httpx.Client() as client:
49
- response = client.post(f"{net}/register", json={"handles":handles, "net":net, "api_key":api_key})
50
- data = response.json()
51
- if response.status_code==200:
52
- for i in data:
53
- O(f"{i['status']}/{i['mode']}", f"{net}/{i['handle']}" if i["status"] != "taken" else "")
54
- return True
55
- else:
56
- O("failed", data.get("error"))
57
- return False
58
- except Exception as e:
59
- O("error",e)
60
- return False
61
-
62
- class Cycls:
63
- def __init__(self, url="", net="https://cycls.com", port=find_available_port(8001), api_key=None):
64
- import uuid
65
- self.subdomain = str(uuid.uuid4())[:8] #!
66
- self.server = FastAPI()
67
- self.net = net
68
- self.port = port
69
- self.url = url
70
- self.apps = {}
71
- self.api_key = api_key
72
-
73
- def __call__(self, handle):
74
- def decorator(func):
75
- @wraps(func)
76
- async def async_wrapper(*args, **kwargs):
77
- return StreamingResponse(await func(*args, **kwargs))
78
- @wraps(func)
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
- async def gateway(self, request: Request):
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
- handle = data.get('handle')
89
- if handle in self.apps:
90
- func = self.apps[handle]
91
- message = Message(**data)
92
- return await func(message) if inspect.iscoroutinefunction(func) else func(message)
93
- return {"error": "Handle not found"}
94
-
95
- def push(self):
96
- O("port",self.port)
97
- if self.url=="":
98
- # self.url = f"https://{self.subdomain}-cycls.tuns.sh"
99
- self.url = f"https://cycls-{self.subdomain}.tuns.sh"
100
- mode = "dev"
101
- else:
102
- mode = "prod"
103
- config = [{"handle": handle, "url": self.url+"/gateway", "mode":mode} for handle in list(self.apps.keys())]
104
- if on := register(config, self.net, self.api_key):
105
- self.server.post("/gateway")(self.gateway)
106
- @self.server.on_event("startup")
107
- def startup_event():
108
- # asyncio.create_task(create_ssh_tunnel(f"{self.subdomain}-cycls", self.port))
109
- asyncio.create_task(create_ssh_tunnel(f"{self.subdomain}", self.port))
110
- try:
111
- uvicorn.run(self.server, host="127.0.0.1", port=self.port, log_level="error")
112
- except KeyboardInterrupt:
113
- print(" ");O("exit","done")
114
-
115
- async def call(self, handle, content):
116
- data = {"handle":handle, "content":content, "session":{}, "agent":"yes"}
117
- try:
118
- url = f"{self.net}/stream/"
119
- async with httpx.AsyncClient(timeout=20) as client, client.stream("POST", url, json=data) as response:
120
- if response.status_code != 200:
121
- print("http error")
122
- async for token in response.aiter_text():
123
- yield token
124
- except Exception as e:
125
- print("Exception", e)
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