cycls 0.0.2.24__tar.gz β†’ 0.0.2.31__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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: cycls
3
+ Version: 0.0.2.31
4
+ Summary: Cycls SDK
5
+ Author: Mohammed J. AlRujayi
6
+ Author-email: mj@cycls.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: fastapi (>=0.111.0,<0.112.0)
16
+ Requires-Dist: httpx (>=0.27.0,<0.28.0)
17
+ Requires-Dist: jwt (>=1.4.0,<2.0.0)
18
+ Requires-Dist: modal (>=1.1.0,<2.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ <h3 align="center">
22
+ The Distribution SDK for AI Agents.
23
+ </h3>
24
+
25
+ <h4 align="center">
26
+ <a href="https://cycls.com">Website</a> |
27
+ <a href="https://docs.cycls.com">Docs</a>
28
+ </h4>
29
+
30
+ <h4 align="center">
31
+ <a href="https://pypi.python.org/pypi/cycls"><img src="https://img.shields.io/pypi/v/cycls.svg?label=cycls+pypi&color=blueviolet" alt="cycls Python package on PyPi" /></a>
32
+ <a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
33
+ <a href="https://x.com/cyclsai">
34
+ <img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
35
+ </a>
36
+ </h4>
37
+
38
+
39
+ # Cycls 🚲
40
+
41
+ `cycls` is a zero-config framework for building and publishing AI agents. With a single decorator and one command, you can deploy your code as a web application complete with a front-end UI and an OpenAI-compatible API endpoint.
42
+
43
+ ### Design Philosophy
44
+ `cycls` is an anti-framework. We treat the boilerplate, config files, and infrastructure that surround modern applications as a bug to be eliminated. A developer's focus is the most valuable resource, and context-switching is its greatest enemy.
45
+
46
+ Our zero-config approach makes your Python script the single source of truth for the entire application. When your code is all you need, you stay focused, iterate faster, and ship with confidence.
47
+
48
+ This philosophy has a powerful side-effect: it makes development genuinely iterative. The self-contained nature of an agent encourages you to 'build in cycles'β€”starting simple and adding complexity without penalty. This same simplicity also makes `cycls` an ideal target for code generation. Because the entire application can be expressed in one file, LLMs can write, modify, and reason about `cycls` agents far more effectively than with traditional frameworks. It's a seamless interface for both human and machine.
49
+
50
+
51
+ ## Key Features
52
+
53
+ * ✨ **Zero-Config Deployment:** No YAML or Dockerfiles. `cycls` infers your dependencies, and APIs directly from your Python code.
54
+ * πŸš€ **One-Command Push to Cloud:** Go from local code to a globally scalable, serverless application with a single `agent.push()`.
55
+ * πŸ’» **Instant Local Testing:** Run `agent.run()` to spin up a local server with hot-reloading for rapid iteration and debugging.
56
+ * πŸ€– **OpenAI-Compatible API:** Automatically serves a streaming `/chat/completions` endpoint.
57
+ * 🌐 **Automatic Web UI:** Get a clean, interactive front-end for your agent out of the box, with no front-end code required.
58
+ * πŸ” **Built-in Authentication:** Secure your agent for production with a simple `auth=True` flag that enables JWT-based authentication.
59
+ * πŸ“¦ **Declarative Dependencies:** Define all your `pip`, `apt`, or local file dependencies directly in Python.
60
+
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install cycls
66
+ ```
67
+
68
+ ## How to Use
69
+ ### 1. Local Development: "Hello, World!"
70
+
71
+ Create a file main.py. This simple example creates an agent that streams back the message "hi".
72
+
73
+ ```py
74
+ import cycls
75
+
76
+ # Initialize the agent
77
+ agent = cycls.Agent()
78
+
79
+ # Decorate your function to register it as an agent
80
+ @agent()
81
+ async def hello(context):
82
+ yield "hi"
83
+
84
+ agent.run()
85
+ ```
86
+
87
+ Run it from your terminal:
88
+
89
+ ```bash
90
+ python main.py
91
+ ```
92
+ This will start a local server. Open your browser to http://127.0.0.1:8000 to interact with your agent.
93
+
94
+ ### 2. Cloud Deployment: An OpenAI-Powered Agent
95
+ This example creates a more advanced agent that calls the OpenAI API. It will be deployed to the cloud with authentication enabled.
96
+
97
+ ```py
98
+ # deploy.py
99
+ import cycls
100
+
101
+ # Initialize the agent with dependencies and API keys
102
+ agent = cycls.Agent(
103
+ pip=["openai"],
104
+ keys=["ak-<token_id>", "as-<token_secret>"]
105
+ )
106
+
107
+ # A helper function to call the LLM
108
+ async def llm(messages):
109
+ # Import inside the function: 'openai' is only needed at runtime in the container.
110
+ import openai
111
+ client = openai.AsyncOpenAI(api_key="sk-...") # Your OpenAI key
112
+ model = "gpt-4o"
113
+ response = await client.chat.completions.create(
114
+ model=model,
115
+ messages=messages,
116
+ temperature=1.0,
117
+ stream=True
118
+ )
119
+ # Yield the content from the streaming response
120
+ async def event_stream():
121
+ async for chunk in response:
122
+ content = chunk.choices[0].delta.content
123
+ if content:
124
+ yield content
125
+ return event_stream()
126
+
127
+ # Register the function as an agent named "cake" and enable auth
128
+ @agent("cake", auth=True)
129
+ async def cake_agent(context):
130
+ # The context object contains the message history
131
+ return await llm(context.messages)
132
+
133
+ # Deploy the agent to the cloud
134
+ agent.push(prod=True)
135
+ ```
136
+
137
+ Run the deployment command from your terminal:
138
+
139
+ ```bash
140
+ python main.py
141
+ ```
142
+ After a few moments, your agent will be live and accessible at a public URL like https://cake.cycls.ai.
143
+
144
+ ### License
145
+ This project is licensed under the MIT License.
146
+
@@ -0,0 +1,125 @@
1
+ <h3 align="center">
2
+ The Distribution SDK for AI Agents.
3
+ </h3>
4
+
5
+ <h4 align="center">
6
+ <a href="https://cycls.com">Website</a> |
7
+ <a href="https://docs.cycls.com">Docs</a>
8
+ </h4>
9
+
10
+ <h4 align="center">
11
+ <a href="https://pypi.python.org/pypi/cycls"><img src="https://img.shields.io/pypi/v/cycls.svg?label=cycls+pypi&color=blueviolet" alt="cycls Python package on PyPi" /></a>
12
+ <a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
13
+ <a href="https://x.com/cyclsai">
14
+ <img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
15
+ </a>
16
+ </h4>
17
+
18
+
19
+ # Cycls 🚲
20
+
21
+ `cycls` is a zero-config framework for building and publishing AI agents. With a single decorator and one command, you can deploy your code as a web application complete with a front-end UI and an OpenAI-compatible API endpoint.
22
+
23
+ ### Design Philosophy
24
+ `cycls` is an anti-framework. We treat the boilerplate, config files, and infrastructure that surround modern applications as a bug to be eliminated. A developer's focus is the most valuable resource, and context-switching is its greatest enemy.
25
+
26
+ Our zero-config approach makes your Python script the single source of truth for the entire application. When your code is all you need, you stay focused, iterate faster, and ship with confidence.
27
+
28
+ This philosophy has a powerful side-effect: it makes development genuinely iterative. The self-contained nature of an agent encourages you to 'build in cycles'β€”starting simple and adding complexity without penalty. This same simplicity also makes `cycls` an ideal target for code generation. Because the entire application can be expressed in one file, LLMs can write, modify, and reason about `cycls` agents far more effectively than with traditional frameworks. It's a seamless interface for both human and machine.
29
+
30
+
31
+ ## Key Features
32
+
33
+ * ✨ **Zero-Config Deployment:** No YAML or Dockerfiles. `cycls` infers your dependencies, and APIs directly from your Python code.
34
+ * πŸš€ **One-Command Push to Cloud:** Go from local code to a globally scalable, serverless application with a single `agent.push()`.
35
+ * πŸ’» **Instant Local Testing:** Run `agent.run()` to spin up a local server with hot-reloading for rapid iteration and debugging.
36
+ * πŸ€– **OpenAI-Compatible API:** Automatically serves a streaming `/chat/completions` endpoint.
37
+ * 🌐 **Automatic Web UI:** Get a clean, interactive front-end for your agent out of the box, with no front-end code required.
38
+ * πŸ” **Built-in Authentication:** Secure your agent for production with a simple `auth=True` flag that enables JWT-based authentication.
39
+ * πŸ“¦ **Declarative Dependencies:** Define all your `pip`, `apt`, or local file dependencies directly in Python.
40
+
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install cycls
46
+ ```
47
+
48
+ ## How to Use
49
+ ### 1. Local Development: "Hello, World!"
50
+
51
+ Create a file main.py. This simple example creates an agent that streams back the message "hi".
52
+
53
+ ```py
54
+ import cycls
55
+
56
+ # Initialize the agent
57
+ agent = cycls.Agent()
58
+
59
+ # Decorate your function to register it as an agent
60
+ @agent()
61
+ async def hello(context):
62
+ yield "hi"
63
+
64
+ agent.run()
65
+ ```
66
+
67
+ Run it from your terminal:
68
+
69
+ ```bash
70
+ python main.py
71
+ ```
72
+ This will start a local server. Open your browser to http://127.0.0.1:8000 to interact with your agent.
73
+
74
+ ### 2. Cloud Deployment: An OpenAI-Powered Agent
75
+ This example creates a more advanced agent that calls the OpenAI API. It will be deployed to the cloud with authentication enabled.
76
+
77
+ ```py
78
+ # deploy.py
79
+ import cycls
80
+
81
+ # Initialize the agent with dependencies and API keys
82
+ agent = cycls.Agent(
83
+ pip=["openai"],
84
+ keys=["ak-<token_id>", "as-<token_secret>"]
85
+ )
86
+
87
+ # A helper function to call the LLM
88
+ async def llm(messages):
89
+ # Import inside the function: 'openai' is only needed at runtime in the container.
90
+ import openai
91
+ client = openai.AsyncOpenAI(api_key="sk-...") # Your OpenAI key
92
+ model = "gpt-4o"
93
+ response = await client.chat.completions.create(
94
+ model=model,
95
+ messages=messages,
96
+ temperature=1.0,
97
+ stream=True
98
+ )
99
+ # Yield the content from the streaming response
100
+ async def event_stream():
101
+ async for chunk in response:
102
+ content = chunk.choices[0].delta.content
103
+ if content:
104
+ yield content
105
+ return event_stream()
106
+
107
+ # Register the function as an agent named "cake" and enable auth
108
+ @agent("cake", auth=True)
109
+ async def cake_agent(context):
110
+ # The context object contains the message history
111
+ return await llm(context.messages)
112
+
113
+ # Deploy the agent to the cloud
114
+ agent.push(prod=True)
115
+ ```
116
+
117
+ Run the deployment command from your terminal:
118
+
119
+ ```bash
120
+ python main.py
121
+ ```
122
+ After a few moments, your agent will be live and accessible at a public URL like https://cake.cycls.ai.
123
+
124
+ ### License
125
+ This project is licensed under the MIT License.
@@ -0,0 +1 @@
1
+ from .cycls import Agent
@@ -0,0 +1,168 @@
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"})
79
+
80
+ @app.post("/")
81
+ @app.post("/chat/completions")
82
+ async def back(request: Request, jwt: Optional[dict] = Depends(validate) if auth else None):
83
+ data = await request.json()
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, theme=theme_path, org=None, api_token=None, pip=[], apt=[], copy=[], keys=["",""]):
104
+ self.org, self.api_token = org, api_token
105
+ self.theme = theme
106
+ self.keys, self.pip, self.apt, self.copy = keys, pip, apt, copy
107
+
108
+ self.registered_functions = []
109
+
110
+ def __call__(self, name="", header="", intro="", domain=None, auth=False):
111
+ def decorator(f):
112
+ self.registered_functions.append({
113
+ "func": f,
114
+ "config": ["public", False, self.org, self.api_token, header, intro, auth],
115
+ "name": name,
116
+ "domain": domain or f"{name}.cycls.ai",
117
+ })
118
+ return f
119
+ return decorator
120
+
121
+ def run(self, port=8000):
122
+ if not self.registered_functions:
123
+ print("Error: No @agent decorated function found.")
124
+ return
125
+
126
+ i = self.registered_functions[0]
127
+ if len(self.registered_functions) > 1:
128
+ print(f"⚠️ Warning: Multiple agents found. Running '{i['name']}'.")
129
+ print(f"πŸš€ Starting local server at localhost:{port}")
130
+ i["config"][0], i["config"][6] = self.theme, False
131
+ uvicorn.run(web(i["func"], *i["config"]), host="0.0.0.0", port=port)
132
+ return
133
+
134
+ def push(self, prod=False):
135
+ self.client = modal.Client.from_credentials(*self.keys)
136
+ image = (modal.Image.debian_slim()
137
+ .pip_install("fastapi[standard]", "pyjwt", "cryptography", *self.pip)
138
+ .apt_install(*self.apt)
139
+ .add_local_dir(self.theme, "/root/public")
140
+ .add_local_python_source("cycls"))
141
+ for item in self.copy:
142
+ image = image.add_local_file(item, f"/root/{item}") if "." in item else image.add_local_dir(item, f'/root/{item}')
143
+ self.app = modal.App("development", image=image)
144
+
145
+ if not self.registered_functions:
146
+ print("Error: No @agent decorated function found.")
147
+ return
148
+
149
+ for i in self.registered_functions:
150
+ i["config"][1] = True if prod else False
151
+ self.app.function(serialized=True, name=i["name"])(
152
+ modal.asgi_app(label=i["name"], custom_domains=[i["domain"]])
153
+ (lambda: web(i["func"], *i["config"]))
154
+ )
155
+ if prod:
156
+ for i in self.registered_functions:
157
+ print(f"βœ… Deployed to β‡’ https://{i['domain']}")
158
+ self.app.deploy(client=self.client, name=self.registered_functions[0]["name"])
159
+ return
160
+ else:
161
+ with modal.enable_output():
162
+ run_app(app=self.app, client=self.client)
163
+ print(" Modal development server is running. Press Ctrl+C to stop.")
164
+ with modal.enable_output(), run_app(app=self.app, client=self.client):
165
+ while True: time.sleep(10)
166
+
167
+ # poetry run python agent.py
168
+ # poetry publish --build