cycls 0.0.2.72__tar.gz → 0.0.2.74__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.72
3
+ Version: 0.0.2.74
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -32,6 +32,7 @@ Distribute Intelligence
32
32
 
33
33
  <h4 align="center">
34
34
  <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>
35
+ <a href="https://github.com/Cycls/cycls/actions/workflows/tests.yml"><img src="https://github.com/Cycls/cycls/actions/workflows/tests.yml/badge.svg" alt="Tests" /></a>
35
36
  <a href="https://blog.cycls.com"><img src="https://img.shields.io/badge/newsletter-blueviolet.svg?logo=substack&label=cycls" alt="Cycls newsletter" /></a>
36
37
  <a href="https://x.com/cyclsai">
37
38
  <img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
@@ -46,31 +47,32 @@ The open-source SDK for distributing AI agents.
46
47
 
47
48
  ## Distribute Intelligence
48
49
 
49
- AI capabilities shouldn't be locked in notebooks or trapped behind months of infrastructure work. Cycls turns your Python functions into production services - complete with APIs, interfaces, auth, and analytics. You focus on the intelligence. Cycls handles the distribution.
50
-
51
50
  Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
52
51
 
53
52
  ```python
54
53
  import cycls
55
54
 
56
- agent = cycls.Agent(pip=["openai"])
55
+ cycls.api_key = "YOUR_CYCLS_API_KEY"
57
56
 
58
- @agent("my-agent", auth=True, analytics=True)
59
- async def chat(context):
57
+ @cycls.agent(pip=["openai"])
58
+ async def agent(context):
60
59
  from openai import AsyncOpenAI
61
60
  client = AsyncOpenAI()
62
61
 
63
- response = await client.chat.completions.create(
64
- model="gpt-4o",
65
- messages=context.messages,
66
- stream=True
62
+ stream = await client.responses.create(
63
+ model="o3-mini",
64
+ input=context.messages,
65
+ stream=True,
66
+ reasoning={"effort": "medium", "summary": "auto"},
67
67
  )
68
68
 
69
- async for chunk in response:
70
- if chunk.choices[0].delta.content:
71
- yield chunk.choices[0].delta.content
69
+ async for event in stream:
70
+ if event.type == "response.reasoning_summary_text.delta":
71
+ yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
72
+ elif event.type == "response.output_text.delta":
73
+ yield event.delta
72
74
 
73
- agent.deploy() # Live at https://my-agent.cycls.ai
75
+ agent.deploy() # Live at https://agent.cycls.ai
74
76
  ```
75
77
 
76
78
  ## Installation
@@ -87,7 +89,7 @@ Requires Docker.
87
89
  - **Web Interface** - Chat UI served automatically
88
90
  - **Authentication** - `auth=True` enables JWT-based access control
89
91
  - **Analytics** - `analytics=True` tracks usage
90
- - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
92
+ - **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
91
93
  - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
92
94
 
93
95
  ## Running
@@ -95,17 +97,33 @@ Requires Docker.
95
97
  ```python
96
98
  agent.local() # Development with hot-reload (localhost:8080)
97
99
  agent.local(watch=False) # Development without hot-reload
98
- agent.deploy() # Production: https://agent-name.cycls.ai
100
+ agent.deploy() # Production: https://agent.cycls.ai
99
101
  ```
100
102
 
101
103
  Get an API key at [cycls.com](https://cycls.com).
102
104
 
105
+ ## Authentication & Analytics
106
+
107
+ ```python
108
+ @cycls.agent(pip=["openai"], auth=True, analytics=True)
109
+ async def agent(context):
110
+ # context.user available when auth=True
111
+ user = context.user # User(id, email, name, plans)
112
+ yield f"Hello {user.name}!"
113
+ ```
114
+
115
+ | Flag | Description |
116
+ |------|-------------|
117
+ | `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
118
+ | `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
119
+ | `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
120
+
103
121
  ## Native UI Components
104
122
 
105
123
  Yield structured objects for rich streaming responses:
106
124
 
107
125
  ```python
108
- @agent()
126
+ @cycls.agent()
109
127
  async def demo(context):
110
128
  yield {"type": "thinking", "thinking": "Analyzing the request..."}
111
129
  yield "Here's what I found:\n\n"
@@ -128,32 +146,26 @@ async def demo(context):
128
146
  | `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
129
147
  | `{"type": "image", "src": "..."}` | Yes |
130
148
 
131
- ### Reasoning Models
149
+ ### Thinking Bubbles
132
150
 
133
- ```python
134
- @agent()
135
- async def chat(context):
136
- from openai import AsyncOpenAI
137
- client = AsyncOpenAI()
151
+ The `{"type": "thinking", "thinking": "..."}` component renders as a collapsible thinking bubble in the UI. Each yield appends to the same bubble until a different component type is yielded:
138
152
 
139
- stream = await client.responses.create(
140
- model="o3-mini",
141
- input=context.messages,
142
- stream=True,
143
- reasoning={"effort": "medium", "summary": "auto"},
144
- )
153
+ ```python
154
+ # Multiple yields build one thinking bubble
155
+ yield {"type": "thinking", "thinking": "Let me "}
156
+ yield {"type": "thinking", "thinking": "analyze this..."}
157
+ yield {"type": "thinking", "thinking": " Done thinking."}
145
158
 
146
- async for event in stream:
147
- if event.type == "response.reasoning_summary_text.delta":
148
- yield {"type": "thinking", "thinking": event.delta}
149
- elif event.type == "response.output_text.delta":
150
- yield event.delta
159
+ # Then output the response
160
+ yield "Here's what I found..."
151
161
  ```
152
162
 
163
+ This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
164
+
153
165
  ## Context Object
154
166
 
155
167
  ```python
156
- @agent()
168
+ @cycls.agent()
157
169
  async def chat(context):
158
170
  context.messages # [{"role": "user", "content": "..."}]
159
171
  context.messages.raw # Full data including UI component parts
@@ -183,16 +195,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
183
195
 
184
196
  ## Declarative Infrastructure
185
197
 
186
- Define your entire runtime in Python:
198
+ Define your entire runtime in the decorator:
187
199
 
188
200
  ```python
189
- agent = cycls.Agent(
201
+ @cycls.agent(
190
202
  pip=["openai", "pandas", "numpy"],
191
203
  apt=["ffmpeg", "libmagic1"],
192
- run_commands=["curl -sSL https://example.com/setup.sh | bash"],
193
204
  copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
194
205
  copy_public=["./assets/logo.png", "./static/"],
195
206
  )
207
+ async def my_agent(context):
208
+ ...
196
209
  ```
197
210
 
198
211
  ### `pip` - Python Packages
@@ -211,17 +224,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
211
224
  apt=["ffmpeg", "imagemagick", "libpq-dev"]
212
225
  ```
213
226
 
214
- ### `run_commands` - Shell Commands
215
-
216
- Run arbitrary shell commands during the container build. Useful for custom setup scripts, downloading assets, or any build-time configuration.
217
-
218
- ```python
219
- run_commands=[
220
- "curl -sSL https://example.com/setup.sh | bash",
221
- "chmod +x /app/scripts/*.sh"
222
- ]
223
- ```
224
-
225
227
  ### `copy` - Bundle Files and Directories
226
228
 
227
229
  Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
@@ -237,7 +239,7 @@ copy=[
237
239
  Then import them in your function:
238
240
 
239
241
  ```python
240
- @agent()
242
+ @cycls.agent(copy=["./utils.py"])
241
243
  async def chat(context):
242
244
  from utils import helper_function # Your bundled module
243
245
  ...
@@ -9,6 +9,7 @@ Distribute Intelligence
9
9
 
10
10
  <h4 align="center">
11
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://github.com/Cycls/cycls/actions/workflows/tests.yml"><img src="https://github.com/Cycls/cycls/actions/workflows/tests.yml/badge.svg" alt="Tests" /></a>
12
13
  <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
14
  <a href="https://x.com/cyclsai">
14
15
  <img src="https://img.shields.io/twitter/follow/CyclsAI" alt="Cycls Twitter" />
@@ -23,31 +24,32 @@ The open-source SDK for distributing AI agents.
23
24
 
24
25
  ## Distribute Intelligence
25
26
 
26
- AI capabilities shouldn't be locked in notebooks or trapped behind months of infrastructure work. Cycls turns your Python functions into production services - complete with APIs, interfaces, auth, and analytics. You focus on the intelligence. Cycls handles the distribution.
27
-
28
27
  Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
29
28
 
30
29
  ```python
31
30
  import cycls
32
31
 
33
- agent = cycls.Agent(pip=["openai"])
32
+ cycls.api_key = "YOUR_CYCLS_API_KEY"
34
33
 
35
- @agent("my-agent", auth=True, analytics=True)
36
- async def chat(context):
34
+ @cycls.agent(pip=["openai"])
35
+ async def agent(context):
37
36
  from openai import AsyncOpenAI
38
37
  client = AsyncOpenAI()
39
38
 
40
- response = await client.chat.completions.create(
41
- model="gpt-4o",
42
- messages=context.messages,
43
- stream=True
39
+ stream = await client.responses.create(
40
+ model="o3-mini",
41
+ input=context.messages,
42
+ stream=True,
43
+ reasoning={"effort": "medium", "summary": "auto"},
44
44
  )
45
45
 
46
- async for chunk in response:
47
- if chunk.choices[0].delta.content:
48
- yield chunk.choices[0].delta.content
46
+ async for event in stream:
47
+ if event.type == "response.reasoning_summary_text.delta":
48
+ yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
49
+ elif event.type == "response.output_text.delta":
50
+ yield event.delta
49
51
 
50
- agent.deploy() # Live at https://my-agent.cycls.ai
52
+ agent.deploy() # Live at https://agent.cycls.ai
51
53
  ```
52
54
 
53
55
  ## Installation
@@ -64,7 +66,7 @@ Requires Docker.
64
66
  - **Web Interface** - Chat UI served automatically
65
67
  - **Authentication** - `auth=True` enables JWT-based access control
66
68
  - **Analytics** - `analytics=True` tracks usage
67
- - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
69
+ - **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
68
70
  - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
69
71
 
70
72
  ## Running
@@ -72,17 +74,33 @@ Requires Docker.
72
74
  ```python
73
75
  agent.local() # Development with hot-reload (localhost:8080)
74
76
  agent.local(watch=False) # Development without hot-reload
75
- agent.deploy() # Production: https://agent-name.cycls.ai
77
+ agent.deploy() # Production: https://agent.cycls.ai
76
78
  ```
77
79
 
78
80
  Get an API key at [cycls.com](https://cycls.com).
79
81
 
82
+ ## Authentication & Analytics
83
+
84
+ ```python
85
+ @cycls.agent(pip=["openai"], auth=True, analytics=True)
86
+ async def agent(context):
87
+ # context.user available when auth=True
88
+ user = context.user # User(id, email, name, plans)
89
+ yield f"Hello {user.name}!"
90
+ ```
91
+
92
+ | Flag | Description |
93
+ |------|-------------|
94
+ | `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
95
+ | `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
96
+ | `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
97
+
80
98
  ## Native UI Components
81
99
 
82
100
  Yield structured objects for rich streaming responses:
83
101
 
84
102
  ```python
85
- @agent()
103
+ @cycls.agent()
86
104
  async def demo(context):
87
105
  yield {"type": "thinking", "thinking": "Analyzing the request..."}
88
106
  yield "Here's what I found:\n\n"
@@ -105,32 +123,26 @@ async def demo(context):
105
123
  | `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
106
124
  | `{"type": "image", "src": "..."}` | Yes |
107
125
 
108
- ### Reasoning Models
126
+ ### Thinking Bubbles
109
127
 
110
- ```python
111
- @agent()
112
- async def chat(context):
113
- from openai import AsyncOpenAI
114
- client = AsyncOpenAI()
128
+ The `{"type": "thinking", "thinking": "..."}` component renders as a collapsible thinking bubble in the UI. Each yield appends to the same bubble until a different component type is yielded:
115
129
 
116
- stream = await client.responses.create(
117
- model="o3-mini",
118
- input=context.messages,
119
- stream=True,
120
- reasoning={"effort": "medium", "summary": "auto"},
121
- )
130
+ ```python
131
+ # Multiple yields build one thinking bubble
132
+ yield {"type": "thinking", "thinking": "Let me "}
133
+ yield {"type": "thinking", "thinking": "analyze this..."}
134
+ yield {"type": "thinking", "thinking": " Done thinking."}
122
135
 
123
- async for event in stream:
124
- if event.type == "response.reasoning_summary_text.delta":
125
- yield {"type": "thinking", "thinking": event.delta}
126
- elif event.type == "response.output_text.delta":
127
- yield event.delta
136
+ # Then output the response
137
+ yield "Here's what I found..."
128
138
  ```
129
139
 
140
+ This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
141
+
130
142
  ## Context Object
131
143
 
132
144
  ```python
133
- @agent()
145
+ @cycls.agent()
134
146
  async def chat(context):
135
147
  context.messages # [{"role": "user", "content": "..."}]
136
148
  context.messages.raw # Full data including UI component parts
@@ -160,16 +172,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
160
172
 
161
173
  ## Declarative Infrastructure
162
174
 
163
- Define your entire runtime in Python:
175
+ Define your entire runtime in the decorator:
164
176
 
165
177
  ```python
166
- agent = cycls.Agent(
178
+ @cycls.agent(
167
179
  pip=["openai", "pandas", "numpy"],
168
180
  apt=["ffmpeg", "libmagic1"],
169
- run_commands=["curl -sSL https://example.com/setup.sh | bash"],
170
181
  copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
171
182
  copy_public=["./assets/logo.png", "./static/"],
172
183
  )
184
+ async def my_agent(context):
185
+ ...
173
186
  ```
174
187
 
175
188
  ### `pip` - Python Packages
@@ -188,17 +201,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
188
201
  apt=["ffmpeg", "imagemagick", "libpq-dev"]
189
202
  ```
190
203
 
191
- ### `run_commands` - Shell Commands
192
-
193
- Run arbitrary shell commands during the container build. Useful for custom setup scripts, downloading assets, or any build-time configuration.
194
-
195
- ```python
196
- run_commands=[
197
- "curl -sSL https://example.com/setup.sh | bash",
198
- "chmod +x /app/scripts/*.sh"
199
- ]
200
- ```
201
-
202
204
  ### `copy` - Bundle Files and Directories
203
205
 
204
206
  Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
@@ -214,7 +216,7 @@ copy=[
214
216
  Then import them in your function:
215
217
 
216
218
  ```python
217
- @agent()
219
+ @cycls.agent(copy=["./utils.py"])
218
220
  async def chat(context):
219
221
  from utils import helper_function # Your bundled module
220
222
  ...
@@ -0,0 +1,20 @@
1
+ import sys
2
+ from types import ModuleType
3
+ from .sdk import function, agent
4
+ from .runtime import Runtime
5
+
6
+ class _Module(ModuleType):
7
+ def __getattr__(self, name):
8
+ from . import sdk
9
+ if name in ("api_key", "base_url"):
10
+ return getattr(sdk, name)
11
+ raise AttributeError(f"module 'cycls' has no attribute '{name}'")
12
+
13
+ def __setattr__(self, name, value):
14
+ from . import sdk
15
+ if name in ("api_key", "base_url"):
16
+ setattr(sdk, name, value)
17
+ return
18
+ super().__setattr__(name, value)
19
+
20
+ sys.modules[__name__].__class__ = _Module
@@ -0,0 +1,181 @@
1
+ import os, time, uvicorn
2
+ from .runtime import Runtime
3
+ from .web import web, Config
4
+ from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
5
+ import importlib.resources
6
+
7
+ CYCLS_PATH = importlib.resources.files('cycls')
8
+
9
+ # Module-level configuration
10
+ api_key = None
11
+ base_url = None
12
+
13
+ themes = {
14
+ "default": CYCLS_PATH.joinpath('default-theme'),
15
+ "dev": CYCLS_PATH.joinpath('dev-theme'),
16
+ }
17
+
18
+ def _resolve_theme(theme):
19
+ """Resolve theme - accepts string name or path"""
20
+ if isinstance(theme, str):
21
+ if theme in themes:
22
+ return themes[theme]
23
+ raise ValueError(f"Unknown theme: {theme}. Available: {list(themes.keys())}")
24
+ return theme
25
+
26
+ def _set_prod(config: Config, prod: bool):
27
+ config.prod = prod
28
+ config.pk = PK_LIVE if prod else PK_TEST
29
+ config.jwks = JWKS_PROD if prod else JWKS_TEST
30
+
31
+ class AgentRuntime:
32
+ """Wraps an agent function with local/deploy/modal capabilities."""
33
+
34
+ def __init__(self, func, name, theme, pip, apt, copy, copy_public, modal_keys, auth, org, domain, header, intro, title, plan, analytics):
35
+ self.func = func
36
+ self.name = name
37
+ self.theme = _resolve_theme(theme)
38
+ self.pip = pip
39
+ self.apt = apt
40
+ self.copy = copy
41
+ self.copy_public = copy_public
42
+ self.modal_keys = modal_keys
43
+ self.domain = domain or f"{name}.cycls.ai"
44
+
45
+ self.config = Config(
46
+ header=header,
47
+ intro=intro,
48
+ title=title,
49
+ auth=auth,
50
+ plan=plan,
51
+ analytics=analytics,
52
+ org=org,
53
+ )
54
+
55
+ def __call__(self, *args, **kwargs):
56
+ """Make the runtime callable - delegates to the wrapped function."""
57
+ return self.func(*args, **kwargs)
58
+
59
+ def _local(self, port=8080, watch=True):
60
+ """Run directly with uvicorn (no Docker)."""
61
+ print(f"Starting local server at localhost:{port}")
62
+ self.config.public_path = self.theme
63
+ _set_prod(self.config, False)
64
+ uvicorn.run(web(self.func, self.config), host="0.0.0.0", port=port, reload=watch)
65
+
66
+ def _runtime(self, prod=False):
67
+ """Create a Runtime instance for deployment."""
68
+ _set_prod(self.config, prod)
69
+ config_dict = self.config.model_dump()
70
+
71
+ # Extract to local variables to avoid capturing self in lambda (cloudpickle issue)
72
+ func = self.func
73
+ name = self.name
74
+
75
+ files = {str(self.theme): "theme", str(CYCLS_PATH)+"/web.py": "web.py"}
76
+ files.update({f: f for f in self.copy})
77
+ files.update({f: f"public/{f}" for f in self.copy_public})
78
+
79
+ return Runtime(
80
+ func=lambda port: __import__("web").serve(func, config_dict, name, port),
81
+ name=name,
82
+ apt_packages=self.apt,
83
+ pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
84
+ copy=files,
85
+ base_url=base_url,
86
+ api_key=api_key
87
+ )
88
+
89
+ def local(self, port=8080, watch=True):
90
+ """Run locally in Docker with file watching by default."""
91
+ if os.environ.get('_CYCLS_WATCH_CHILD'):
92
+ watch = False
93
+ runtime = self._runtime(prod=False)
94
+ runtime.watch(port=port) if watch else runtime.run(port=port)
95
+
96
+ def deploy(self, port=8080):
97
+ """Deploy to production."""
98
+ if api_key is None:
99
+ print("Error: Please set cycls.api_key")
100
+ return
101
+ runtime = self._runtime(prod=True)
102
+ runtime.deploy(port=port)
103
+
104
+ def modal(self, prod=False):
105
+ import modal
106
+ from modal.runner import run_app
107
+
108
+ # Extract to local variables to avoid capturing self in lambda
109
+ func = self.func
110
+ name = self.name
111
+ domain = self.domain
112
+
113
+ client = modal.Client.from_credentials(*self.modal_keys)
114
+ image = (modal.Image.debian_slim()
115
+ .pip_install("fastapi[standard]", "pyjwt", "cryptography", *self.pip)
116
+ .apt_install(*self.apt)
117
+ .add_local_dir(self.theme, "/root/theme")
118
+ .add_local_file(str(CYCLS_PATH)+"/web.py", "/root/web.py"))
119
+
120
+ for item in self.copy:
121
+ image = image.add_local_file(item, f"/root/{item}") if "." in item else image.add_local_dir(item, f'/root/{item}')
122
+
123
+ for item in self.copy_public:
124
+ image = image.add_local_file(item, f"/root/public/{item}") if "." in item else image.add_local_dir(item, f'/root/public/{item}')
125
+
126
+ app = modal.App("development", image=image)
127
+
128
+ _set_prod(self.config, prod)
129
+ config_dict = self.config.model_dump()
130
+
131
+ app.function(serialized=True, name=name)(
132
+ modal.asgi_app(label=name, custom_domains=[domain])
133
+ (lambda: __import__("web").web(func, config_dict))
134
+ )
135
+
136
+ if prod:
137
+ print(f"Deployed to => https://{domain}")
138
+ app.deploy(client=client, name=name)
139
+ else:
140
+ with modal.enable_output():
141
+ run_app(app=app, client=client)
142
+ print("Modal development server is running. Press Ctrl+C to stop.")
143
+ with modal.enable_output(), run_app(app=app, client=client):
144
+ while True: time.sleep(10)
145
+
146
+
147
+ def agent(name=None, pip=[], apt=[], copy=[], copy_public=[], theme="default", modal_keys=["", ""], auth=False, org=None, domain=None, header="", intro="", title="", plan="free", analytics=False):
148
+ """Decorator that transforms a function into a deployable agent."""
149
+ if plan == "cycls_pass":
150
+ auth = True
151
+ analytics = True
152
+
153
+ def decorator(func):
154
+ agent_name = name or func.__name__.replace('_', '-')
155
+ return AgentRuntime(
156
+ func=func,
157
+ name=agent_name,
158
+ theme=theme,
159
+ pip=pip,
160
+ apt=apt,
161
+ copy=copy,
162
+ copy_public=copy_public,
163
+ modal_keys=modal_keys,
164
+ auth=auth,
165
+ org=org,
166
+ domain=domain,
167
+ header=header,
168
+ intro=intro,
169
+ title=title,
170
+ plan=plan,
171
+ analytics=analytics,
172
+ )
173
+ return decorator
174
+
175
+ def function(python_version=None, pip=None, apt=None, run_commands=None, copy=None, name=None):
176
+ """Decorator that transforms a Python function into a containerized, remotely executable object."""
177
+ def decorator(func):
178
+ func_name = name or func.__name__
179
+ copy_dict = {i: i for i in copy or []}
180
+ return Runtime(func, func_name.replace('_', '-'), python_version, pip, apt, run_commands, copy_dict, base_url, api_key)
181
+ return decorator
@@ -10,7 +10,7 @@ class Config(BaseModel):
10
10
  title: str = ""
11
11
  prod: bool = False
12
12
  auth: bool = False
13
- tier: str = "free"
13
+ plan: str = "free"
14
14
  analytics: bool = False
15
15
  org: Optional[str] = None
16
16
  pk: str = ""
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.72"
3
+ version = "0.0.2.74"
4
4
 
5
5
  packages = [{ include = "cycls" }]
6
6
  include = ["cycls/theme/**/*"]
@@ -19,6 +19,10 @@ cloudpickle = "^3.1.1"
19
19
  [tool.poetry.scripts]
20
20
  cycls = "cycls.cli:main"
21
21
 
22
+ [tool.poetry.group.test.dependencies]
23
+ pytest = "^8.0.0"
24
+ requests = "^2.31.0"
25
+
22
26
  [tool.poetry.extras]
23
27
  modal = ["modal"]
24
28
 
@@ -1,2 +0,0 @@
1
- from .sdk import Agent, function
2
- from .runtime import Runtime
@@ -1,186 +0,0 @@
1
- import os, time, inspect, uvicorn
2
- from .runtime import Runtime
3
- from .web import web, Config
4
- from .auth import PK_LIVE, PK_TEST, JWKS_PROD, JWKS_TEST
5
- import importlib.resources
6
- from pydantic import BaseModel
7
- from typing import Callable
8
-
9
- CYCLS_PATH = importlib.resources.files('cycls')
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
-
22
- themes = {
23
- "default": CYCLS_PATH.joinpath('default-theme'),
24
- "dev": CYCLS_PATH.joinpath('dev-theme'),
25
- }
26
-
27
- def resolve_theme(theme):
28
- """Resolve theme - accepts string name or path"""
29
- if isinstance(theme, str):
30
- if theme in themes:
31
- return themes[theme]
32
- raise ValueError(f"Unknown theme: {theme}. Available: {list(themes.keys())}")
33
- return theme
34
-
35
- def function(python_version=None, pip=None, apt=None, run_commands=None, copy=None, name=None, base_url=None, key=None):
36
- # """
37
- # A decorator factory that transforms a Python function into a containerized,
38
- # remotely executable object.
39
- def decorator(func):
40
- Name = name or func.__name__
41
- copy_dict = {i:i for i in copy or []}
42
- return Runtime(func, Name.replace('_', '-'), python_version, pip, apt, run_commands, copy_dict, base_url, key)
43
- return decorator
44
-
45
- class Agent:
46
- def __init__(self, theme="default", org=None, api_token=None, pip=[], apt=[], copy=[], copy_public=[], modal_keys=["",""], key=None, base_url=None):
47
- self.org, self.api_token = org, api_token
48
- self.theme = resolve_theme(theme)
49
- self.key, self.modal_keys, self.pip, self.apt, self.copy, self.copy_public = key, modal_keys, pip, apt, copy, copy_public
50
- self.base_url = base_url
51
-
52
- self.registered_functions = []
53
-
54
- def __call__(self, name=None, header="", intro="", title="", domain=None, auth=False, tier="free", analytics=False):
55
- if tier=="cycls_pass":
56
- auth=True
57
- analytics=True
58
- def decorator(f):
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
- ))
74
- return f
75
- return decorator
76
-
77
- def _local(self, port=8080, watch=True):
78
- """Run directly with uvicorn (no Docker)."""
79
- if not self.registered_functions:
80
- print("Error: No @agent decorated function found.")
81
- return
82
-
83
- agent = self.registered_functions[0]
84
- if len(self.registered_functions) > 1:
85
- print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
86
- print(f"🚀 Starting local server at localhost:{port}")
87
- agent.config.public_path = self.theme
88
- set_prod(agent.config, False)
89
- uvicorn.run(web(agent.func, agent.config), host="0.0.0.0", port=port, reload=watch)
90
- return
91
-
92
- def _runtime(self, prod=False):
93
- """Create a Runtime instance for the first registered agent."""
94
- if not self.registered_functions:
95
- print("Error: No @agent decorated function found.")
96
- return None
97
-
98
- agent = self.registered_functions[0]
99
- if len(self.registered_functions) > 1:
100
- print(f"⚠️ Warning: Multiple agents found. Running '{agent.name}'.")
101
-
102
- set_prod(agent.config, prod)
103
- func = agent.func
104
- name = agent.name
105
- config_dict = agent.config.model_dump()
106
-
107
- files = {str(self.theme): "theme", str(CYCLS_PATH)+"/web.py": "web.py"}
108
- files.update({f: f for f in self.copy})
109
- files.update({f: f"public/{f}" for f in self.copy_public})
110
-
111
- return Runtime(
112
- func=lambda port: __import__("web").serve(func, config_dict, name, port),
113
- name=name,
114
- apt_packages=self.apt,
115
- pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
116
- copy=files,
117
- base_url=self.base_url,
118
- api_key=self.key
119
- )
120
-
121
- def local(self, port=8080, watch=True):
122
- """Run locally in Docker with file watching by default."""
123
- # Child process spawned by watcher - run without watch
124
- if os.environ.get('_CYCLS_WATCH_CHILD'):
125
- watch = False
126
- runtime = self._runtime(prod=False)
127
- if runtime:
128
- runtime.watch(port=port) if watch else runtime.run(port=port)
129
-
130
- def deploy(self, port=8080):
131
- """Deploy to production."""
132
- if self.key is None:
133
- print("🛑 Error: Please add your Cycls API key")
134
- return
135
- runtime = self._runtime(prod=True)
136
- if runtime:
137
- runtime.deploy(port=port)
138
-
139
- def modal(self, prod=False):
140
- import modal
141
- from modal.runner import run_app
142
- self.client = modal.Client.from_credentials(*self.modal_keys)
143
- image = (modal.Image.debian_slim()
144
- .pip_install("fastapi[standard]", "pyjwt", "cryptography", *self.pip)
145
- .apt_install(*self.apt)
146
- .add_local_dir(self.theme, "/root/theme")
147
- .add_local_file(str(CYCLS_PATH)+"/web.py", "/root/web.py"))
148
-
149
- for item in self.copy:
150
- image = image.add_local_file(item, f"/root/{item}") if "." in item else image.add_local_dir(item, f'/root/{item}')
151
-
152
- for item in self.copy_public:
153
- image = image.add_local_file(item, f"/root/public/{item}") if "." in item else image.add_local_dir(item, f'/root/public/{item}')
154
-
155
- self.app = modal.App("development", image=image)
156
-
157
- if not self.registered_functions:
158
- print("Error: No @agent decorated function found.")
159
- return
160
-
161
- for agent in self.registered_functions:
162
- set_prod(agent.config, prod)
163
- func = agent.func
164
- name = agent.name
165
- domain = agent.domain
166
- config_dict = agent.config.model_dump()
167
- self.app.function(serialized=True, name=name)(
168
- modal.asgi_app(label=name, custom_domains=[domain])
169
- (lambda: __import__("web").web(func, config_dict))
170
- )
171
- if prod:
172
- for agent in self.registered_functions:
173
- print(f"✅ Deployed to ⇒ https://{agent.domain}")
174
- self.app.deploy(client=self.client, name=self.registered_functions[0].name)
175
- return
176
- else:
177
- with modal.enable_output():
178
- run_app(app=self.app, client=self.client)
179
- print(" Modal development server is running. Press Ctrl+C to stop.")
180
- with modal.enable_output(), run_app(app=self.app, client=self.client):
181
- while True: time.sleep(10)
182
-
183
- # docker system prune -af
184
- # poetry config pypi-token.pypi <your-token>
185
- # poetry run python cake.py
186
- # poetry publish --build
File without changes
File without changes
File without changes