cycls 0.0.2.65__tar.gz → 0.0.2.67__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.65 → cycls-0.0.2.67}/PKG-INFO +1 -1
- cycls-0.0.2.67/cycls/auth.py +4 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/sdk.py +65 -35
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/web.py +30 -38
- {cycls-0.0.2.65 → cycls-0.0.2.67}/pyproject.toml +1 -1
- {cycls-0.0.2.65 → cycls-0.0.2.67}/README.md +0 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/__init__.py +0 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/default-theme/assets/index-B0ZKcm_V.css +0 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/default-theme/assets/index-D5EDcI4J.js +0 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/default-theme/index.html +0 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/dev-theme/index.html +0 -0
- {cycls-0.0.2.65 → cycls-0.0.2.67}/cycls/runtime.py +0 -0
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import time, inspect, uvicorn
|
|
2
2
|
from .runtime import Runtime
|
|
3
|
-
from
|
|
4
|
-
from .
|
|
3
|
+
from .web import web, Config
|
|
4
|
+
from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
|
|
5
5
|
import importlib.resources
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from typing import Callable
|
|
6
8
|
|
|
7
9
|
CYCLS_PATH = importlib.resources.files('cycls')
|
|
8
10
|
|
|
11
|
+
class RegisteredAgent(BaseModel):
|
|
12
|
+
func: Callable
|
|
13
|
+
name: str
|
|
14
|
+
domain: str
|
|
15
|
+
config: Config
|
|
16
|
+
|
|
17
|
+
def set_prod(config: Config, prod: bool):
|
|
18
|
+
config.prod = prod
|
|
19
|
+
config.pk = PK_LIVE if prod else PK_TEST
|
|
20
|
+
config.jwks = JWKS_PROD if prod else JWKS_TEST
|
|
21
|
+
|
|
9
22
|
themes = {
|
|
10
23
|
"default": CYCLS_PATH.joinpath('default-theme'),
|
|
11
24
|
"dev": CYCLS_PATH.joinpath('dev-theme'),
|
|
@@ -43,13 +56,21 @@ class Agent:
|
|
|
43
56
|
auth=True
|
|
44
57
|
analytics=True
|
|
45
58
|
def decorator(f):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
agent_name = name or f.__name__.replace('_', '-')
|
|
60
|
+
self.registered_functions.append(RegisteredAgent(
|
|
61
|
+
func=f,
|
|
62
|
+
name=agent_name,
|
|
63
|
+
domain=domain or f"{agent_name}.cycls.ai",
|
|
64
|
+
config=Config(
|
|
65
|
+
header=header,
|
|
66
|
+
intro=intro,
|
|
67
|
+
title=title,
|
|
68
|
+
auth=auth,
|
|
69
|
+
tier=tier,
|
|
70
|
+
analytics=analytics,
|
|
71
|
+
org=self.org,
|
|
72
|
+
),
|
|
73
|
+
))
|
|
53
74
|
return f
|
|
54
75
|
return decorator
|
|
55
76
|
|
|
@@ -57,13 +78,14 @@ class Agent:
|
|
|
57
78
|
if not self.registered_functions:
|
|
58
79
|
print("Error: No @agent decorated function found.")
|
|
59
80
|
return
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
|
|
82
|
+
agent = self.registered_functions[0]
|
|
62
83
|
if len(self.registered_functions) > 1:
|
|
63
|
-
print(f"⚠️ Warning: Multiple agents found. Running '{
|
|
84
|
+
print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
|
|
64
85
|
print(f"🚀 Starting local server at localhost:{port}")
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
agent.config.public_path = self.theme
|
|
87
|
+
set_prod(agent.config, False)
|
|
88
|
+
uvicorn.run(web(agent.func, agent.config), host="0.0.0.0", port=port)
|
|
67
89
|
return
|
|
68
90
|
|
|
69
91
|
def deploy(self, prod=False, port=8080):
|
|
@@ -74,30 +96,34 @@ class Agent:
|
|
|
74
96
|
print("🛑 Error: Please add your Cycls API key")
|
|
75
97
|
return
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
agent = self.registered_functions[0]
|
|
78
100
|
if len(self.registered_functions) > 1:
|
|
79
|
-
print(f"⚠️ Warning: Multiple agents found. Running '{
|
|
101
|
+
print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
set_prod(agent.config, prod)
|
|
104
|
+
func = agent.func
|
|
105
|
+
name = agent.name
|
|
106
|
+
config_dict = agent.config.model_dump()
|
|
83
107
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
files = {str(self.theme): "theme", str(CYCLS_PATH)+"/web.py": "web.py"}
|
|
109
|
+
files.update({f: f for f in self.copy})
|
|
110
|
+
files.update({f: f"public/{f}" for f in self.copy_public})
|
|
87
111
|
|
|
88
112
|
new = Runtime(
|
|
89
|
-
func=lambda port: __import__("web").serve(
|
|
90
|
-
name=
|
|
113
|
+
func=lambda port: __import__("web").serve(func, config_dict, name, port),
|
|
114
|
+
name=name,
|
|
91
115
|
apt_packages=self.apt,
|
|
92
116
|
pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
|
|
93
|
-
copy=
|
|
117
|
+
copy=files,
|
|
94
118
|
base_url=self.base_url,
|
|
95
119
|
api_key=self.key
|
|
96
120
|
)
|
|
97
|
-
new.deploy(port=port) if prod else new.run(port=port)
|
|
121
|
+
new.deploy(port=port) if prod else new.run(port=port)
|
|
98
122
|
return
|
|
99
123
|
|
|
100
124
|
def modal(self, prod=False):
|
|
125
|
+
import modal
|
|
126
|
+
from modal.runner import run_app
|
|
101
127
|
self.client = modal.Client.from_credentials(*self.modal_keys)
|
|
102
128
|
image = (modal.Image.debian_slim()
|
|
103
129
|
.pip_install("fastapi[standard]", "pyjwt", "cryptography", *self.pip)
|
|
@@ -117,22 +143,26 @@ class Agent:
|
|
|
117
143
|
print("Error: No @agent decorated function found.")
|
|
118
144
|
return
|
|
119
145
|
|
|
120
|
-
for
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
for agent in self.registered_functions:
|
|
147
|
+
set_prod(agent.config, prod)
|
|
148
|
+
func = agent.func
|
|
149
|
+
name = agent.name
|
|
150
|
+
domain = agent.domain
|
|
151
|
+
config_dict = agent.config.model_dump()
|
|
152
|
+
self.app.function(serialized=True, name=name)(
|
|
153
|
+
modal.asgi_app(label=name, custom_domains=[domain])
|
|
154
|
+
(lambda: __import__("web").web(func, config_dict))
|
|
125
155
|
)
|
|
126
156
|
if prod:
|
|
127
|
-
for
|
|
128
|
-
print(f"✅ Deployed to ⇒ https://{
|
|
129
|
-
self.app.deploy(client=self.client, name=self.registered_functions[0]
|
|
157
|
+
for agent in self.registered_functions:
|
|
158
|
+
print(f"✅ Deployed to ⇒ https://{agent.domain}")
|
|
159
|
+
self.app.deploy(client=self.client, name=self.registered_functions[0].name)
|
|
130
160
|
return
|
|
131
161
|
else:
|
|
132
162
|
with modal.enable_output():
|
|
133
163
|
run_app(app=self.app, client=self.client)
|
|
134
164
|
print(" Modal development server is running. Press Ctrl+C to stop.")
|
|
135
|
-
with modal.enable_output(), run_app(app=self.app, client=self.client):
|
|
165
|
+
with modal.enable_output(), run_app(app=self.app, client=self.client):
|
|
136
166
|
while True: time.sleep(10)
|
|
137
167
|
|
|
138
168
|
# docker system prune -af
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import json, inspect
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
class Config(BaseModel):
|
|
7
|
+
public_path: str = "theme"
|
|
8
|
+
header: str = ""
|
|
9
|
+
intro: str = ""
|
|
10
|
+
title: str = ""
|
|
11
|
+
prod: bool = False
|
|
12
|
+
auth: bool = False
|
|
13
|
+
tier: str = "free"
|
|
14
|
+
analytics: bool = False
|
|
15
|
+
org: Optional[str] = None
|
|
16
|
+
pk: str = ""
|
|
17
|
+
jwks: str = ""
|
|
8
18
|
|
|
9
19
|
async def openai_encoder(stream):
|
|
10
20
|
if inspect.isasyncgen(stream):
|
|
@@ -48,17 +58,20 @@ class Messages(list):
|
|
|
48
58
|
def raw(self):
|
|
49
59
|
return self._raw
|
|
50
60
|
|
|
51
|
-
def web(func,
|
|
61
|
+
def web(func, config):
|
|
52
62
|
from fastapi import FastAPI, Request, HTTPException, status, Depends
|
|
53
|
-
from fastapi.responses import StreamingResponse
|
|
63
|
+
from fastapi.responses import StreamingResponse
|
|
54
64
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
55
65
|
import jwt
|
|
56
66
|
from jwt import PyJWKClient
|
|
57
|
-
from pydantic import
|
|
67
|
+
from pydantic import EmailStr
|
|
58
68
|
from typing import List, Optional, Any
|
|
59
69
|
from fastapi.staticfiles import StaticFiles
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
if isinstance(config, dict):
|
|
72
|
+
config = Config(**config)
|
|
73
|
+
|
|
74
|
+
jwks = PyJWKClient(config.jwks)
|
|
62
75
|
|
|
63
76
|
class User(BaseModel):
|
|
64
77
|
id: str
|
|
@@ -67,18 +80,6 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
|
|
|
67
80
|
org: Optional[str] = None
|
|
68
81
|
plans: List[str] = []
|
|
69
82
|
|
|
70
|
-
class Metadata(BaseModel):
|
|
71
|
-
header: str
|
|
72
|
-
intro: str
|
|
73
|
-
title: str
|
|
74
|
-
prod: bool
|
|
75
|
-
auth: bool
|
|
76
|
-
tier: str
|
|
77
|
-
analytics: bool
|
|
78
|
-
org: Optional[str]
|
|
79
|
-
pk_live: str
|
|
80
|
-
pk_test: str
|
|
81
|
-
|
|
82
83
|
class Context(BaseModel):
|
|
83
84
|
messages: Any
|
|
84
85
|
user: Optional[User] = None
|
|
@@ -99,7 +100,7 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
|
|
|
99
100
|
@app.post("/")
|
|
100
101
|
@app.post("/chat/cycls")
|
|
101
102
|
@app.post("/chat/completions")
|
|
102
|
-
async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
|
|
103
|
+
async def back(request: Request, jwt: Optional[dict] = Depends(validate) if config.auth else None):
|
|
103
104
|
data = await request.json()
|
|
104
105
|
messages = data.get("messages")
|
|
105
106
|
user_data = jwt.get("user") if jwt else None
|
|
@@ -111,29 +112,20 @@ def web(func, public_path="", prod=False, org=None, api_token=None, header="", i
|
|
|
111
112
|
stream = encoder(stream)
|
|
112
113
|
return StreamingResponse(stream, media_type="text/event-stream")
|
|
113
114
|
|
|
114
|
-
@app.get("/
|
|
115
|
-
async def
|
|
116
|
-
return
|
|
117
|
-
header=header,
|
|
118
|
-
intro=intro,
|
|
119
|
-
title=title,
|
|
120
|
-
prod=prod,
|
|
121
|
-
auth=auth,
|
|
122
|
-
tier=tier,
|
|
123
|
-
analytics=analytics,
|
|
124
|
-
org=org,
|
|
125
|
-
pk_live=PK_LIVE,
|
|
126
|
-
pk_test=PK_TEST
|
|
127
|
-
)
|
|
115
|
+
@app.get("/config")
|
|
116
|
+
async def get_config():
|
|
117
|
+
return config
|
|
128
118
|
|
|
129
119
|
if Path("public").is_dir():
|
|
130
120
|
app.mount("/public", StaticFiles(directory="public", html=True))
|
|
131
|
-
app.mount("/", StaticFiles(directory=public_path, html=True))
|
|
121
|
+
app.mount("/", StaticFiles(directory=config.public_path, html=True))
|
|
132
122
|
|
|
133
123
|
return app
|
|
134
124
|
|
|
135
125
|
def serve(func, config, name, port):
|
|
136
126
|
import uvicorn, logging
|
|
127
|
+
if isinstance(config, dict):
|
|
128
|
+
config = Config(**config)
|
|
137
129
|
logging.getLogger("uvicorn.error").addFilter(lambda r: "0.0.0.0" not in r.getMessage())
|
|
138
130
|
print(f"\n🔨 {name} => http://localhost:{port}\n")
|
|
139
|
-
uvicorn.run(web(func,
|
|
131
|
+
uvicorn.run(web(func, config), host="0.0.0.0", port=port)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|