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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.65
3
+ Version: 0.0.2.67
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -0,0 +1,4 @@
1
+ JWKS_PROD = "https://clerk.cycls.ai/.well-known/jwks.json"
2
+ JWKS_TEST = "https://select-sloth-58.clerk.accounts.dev/.well-known/jwks.json"
3
+ PK_LIVE = "pk_live_Y2xlcmsuY3ljbHMuYWkk"
4
+ PK_TEST = "pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
@@ -1,11 +1,24 @@
1
- import json, time, modal, inspect, uvicorn
1
+ import time, inspect, uvicorn
2
2
  from .runtime import Runtime
3
- from modal.runner import run_app
4
- from .web import web
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
- self.registered_functions.append({
47
- "func": f,
48
- "config": ["theme", False, self.org, self.api_token, header, intro, title, auth, tier, analytics],
49
- # "name": name,
50
- "name": name or (f.__name__).replace('_', '-'),
51
- "domain": domain or f"{name}.cycls.ai",
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
- i = self.registered_functions[0]
81
+
82
+ agent = self.registered_functions[0]
62
83
  if len(self.registered_functions) > 1:
63
- print(f"⚠️ Warning: Multiple agents found. Running '{i['name']}'.")
84
+ print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
64
85
  print(f"🚀 Starting local server at localhost:{port}")
65
- i["config"][0] = self.theme
66
- uvicorn.run(web(i["func"], *i["config"]), host="0.0.0.0", port=port)
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
- i = self.registered_functions[0]
99
+ agent = self.registered_functions[0]
78
100
  if len(self.registered_functions) > 1:
79
- print(f"⚠️ Warning: Multiple agents found. Running '{i['name']}'.")
101
+ print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
80
102
 
81
- # i["config"][1] = False
82
- i["config"][1] = prod
103
+ set_prod(agent.config, prod)
104
+ func = agent.func
105
+ name = agent.name
106
+ config_dict = agent.config.model_dump()
83
107
 
84
- copy={str(self.theme):"theme", str(CYCLS_PATH)+"/web.py":"web.py"}
85
- copy.update({i:i for i in self.copy})
86
- copy.update({i:f"public/{i}" for i in self.copy_public})
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(i["func"], i["config"], i["name"], port),
90
- name=i["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=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 i in self.registered_functions:
121
- i["config"][1] = True if prod else False
122
- self.app.function(serialized=True, name=i["name"])(
123
- modal.asgi_app(label=i["name"], custom_domains=[i["domain"]])
124
- (lambda: __import__("web").web(i["func"], *i["config"]))
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 i in self.registered_functions:
128
- print(f"✅ Deployed to ⇒ https://{i['domain']}")
129
- self.app.deploy(client=self.client, name=self.registered_functions[0]["name"])
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
- JWKS_PROD = "https://clerk.cycls.ai/.well-known/jwks.json"
5
- PK_LIVE = "pk_live_Y2xlcmsuY3ljbHMuYWkk"
6
- JWKS_TEST = "https://select-sloth-58.clerk.accounts.dev/.well-known/jwks.json"
7
- PK_TEST = "pk_test_c2VsZWN0LXNsb3RoLTU4LmNsZXJrLmFjY291bnRzLmRldiQ"
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, public_path="", prod=False, org=None, api_token=None, header="", intro="", title="", auth=False, tier="", analytics=False): # API auth
61
+ def web(func, config):
52
62
  from fastapi import FastAPI, Request, HTTPException, status, Depends
53
- from fastapi.responses import StreamingResponse , HTMLResponse
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 BaseModel, EmailStr
67
+ from pydantic import EmailStr
58
68
  from typing import List, Optional, Any
59
69
  from fastapi.staticfiles import StaticFiles
60
70
 
61
- jwks = PyJWKClient(JWKS_PROD if prod else JWKS_TEST)
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("/metadata")
115
- async def metadata():
116
- return Metadata(
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, *config), host="0.0.0.0", port=port)
131
+ uvicorn.run(web(func, config), host="0.0.0.0", port=port)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.65"
3
+ version = "0.0.2.67"
4
4
 
5
5
  packages = [{ include = "cycls" }]
6
6
  include = ["cycls/theme/**/*"]
File without changes
File without changes
File without changes