cycls 0.0.2.71__tar.gz → 0.0.2.73__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.71
3
+ Version: 0.0.2.73
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" />
@@ -53,24 +54,27 @@ Write a function. Deploy it as an API, a web interface, or both. Add authenticat
53
54
  ```python
54
55
  import cycls
55
56
 
56
- agent = cycls.Agent(pip=["openai"])
57
+ cycls.api_key = "YOUR_CYCLS_API_KEY"
57
58
 
58
- @agent("my-agent", auth=True, analytics=True)
59
- async def chat(context):
59
+ @cycls.agent(pip=["openai"])
60
+ async def agent(context):
60
61
  from openai import AsyncOpenAI
61
62
  client = AsyncOpenAI()
62
63
 
63
- response = await client.chat.completions.create(
64
- model="gpt-4o",
65
- messages=context.messages,
66
- stream=True
64
+ stream = await client.responses.create(
65
+ model="o3-mini",
66
+ input=context.messages,
67
+ stream=True,
68
+ reasoning={"effort": "medium", "summary": "auto"},
67
69
  )
68
70
 
69
- async for chunk in response:
70
- if chunk.choices[0].delta.content:
71
- yield chunk.choices[0].delta.content
71
+ async for event in stream:
72
+ if event.type == "response.reasoning_summary_text.delta":
73
+ yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
74
+ elif event.type == "response.output_text.delta":
75
+ yield event.delta
72
76
 
73
- agent.deploy() # Live at https://my-agent.cycls.ai
77
+ agent.deploy() # Live at https://agent.cycls.ai
74
78
  ```
75
79
 
76
80
  ## Installation
@@ -87,7 +91,7 @@ Requires Docker.
87
91
  - **Web Interface** - Chat UI served automatically
88
92
  - **Authentication** - `auth=True` enables JWT-based access control
89
93
  - **Analytics** - `analytics=True` tracks usage
90
- - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
94
+ - **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
91
95
  - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
92
96
 
93
97
  ## Running
@@ -95,17 +99,33 @@ Requires Docker.
95
99
  ```python
96
100
  agent.local() # Development with hot-reload (localhost:8080)
97
101
  agent.local(watch=False) # Development without hot-reload
98
- agent.deploy() # Production: https://agent-name.cycls.ai
102
+ agent.deploy() # Production: https://agent.cycls.ai
99
103
  ```
100
104
 
101
105
  Get an API key at [cycls.com](https://cycls.com).
102
106
 
107
+ ## Authentication & Analytics
108
+
109
+ ```python
110
+ @cycls.agent(pip=["openai"], auth=True, analytics=True)
111
+ async def agent(context):
112
+ # context.user available when auth=True
113
+ user = context.user # User(id, email, name, plans)
114
+ yield f"Hello {user.name}!"
115
+ ```
116
+
117
+ | Flag | Description |
118
+ |------|-------------|
119
+ | `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
120
+ | `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
121
+ | `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
122
+
103
123
  ## Native UI Components
104
124
 
105
125
  Yield structured objects for rich streaming responses:
106
126
 
107
127
  ```python
108
- @agent()
128
+ @cycls.agent()
109
129
  async def demo(context):
110
130
  yield {"type": "thinking", "thinking": "Analyzing the request..."}
111
131
  yield "Here's what I found:\n\n"
@@ -128,32 +148,26 @@ async def demo(context):
128
148
  | `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
129
149
  | `{"type": "image", "src": "..."}` | Yes |
130
150
 
131
- ### Reasoning Models
151
+ ### Thinking Bubbles
132
152
 
133
- ```python
134
- @agent()
135
- async def chat(context):
136
- from openai import AsyncOpenAI
137
- client = AsyncOpenAI()
153
+ 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
154
 
139
- stream = await client.responses.create(
140
- model="o3-mini",
141
- input=context.messages,
142
- stream=True,
143
- reasoning={"effort": "medium", "summary": "auto"},
144
- )
155
+ ```python
156
+ # Multiple yields build one thinking bubble
157
+ yield {"type": "thinking", "thinking": "Let me "}
158
+ yield {"type": "thinking", "thinking": "analyze this..."}
159
+ yield {"type": "thinking", "thinking": " Done thinking."}
145
160
 
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
161
+ # Then output the response
162
+ yield "Here's what I found..."
151
163
  ```
152
164
 
165
+ This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
166
+
153
167
  ## Context Object
154
168
 
155
169
  ```python
156
- @agent()
170
+ @cycls.agent()
157
171
  async def chat(context):
158
172
  context.messages # [{"role": "user", "content": "..."}]
159
173
  context.messages.raw # Full data including UI component parts
@@ -183,16 +197,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
183
197
 
184
198
  ## Declarative Infrastructure
185
199
 
186
- Define your entire runtime in Python:
200
+ Define your entire runtime in the decorator:
187
201
 
188
202
  ```python
189
- agent = cycls.Agent(
203
+ @cycls.agent(
190
204
  pip=["openai", "pandas", "numpy"],
191
205
  apt=["ffmpeg", "libmagic1"],
192
- run_commands=["curl -sSL https://example.com/setup.sh | bash"],
193
206
  copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
194
207
  copy_public=["./assets/logo.png", "./static/"],
195
208
  )
209
+ async def my_agent(context):
210
+ ...
196
211
  ```
197
212
 
198
213
  ### `pip` - Python Packages
@@ -211,17 +226,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
211
226
  apt=["ffmpeg", "imagemagick", "libpq-dev"]
212
227
  ```
213
228
 
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
229
  ### `copy` - Bundle Files and Directories
226
230
 
227
231
  Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
@@ -237,7 +241,7 @@ copy=[
237
241
  Then import them in your function:
238
242
 
239
243
  ```python
240
- @agent()
244
+ @cycls.agent(copy=["./utils.py"])
241
245
  async def chat(context):
242
246
  from utils import helper_function # Your bundled module
243
247
  ...
@@ -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" />
@@ -30,24 +31,27 @@ Write a function. Deploy it as an API, a web interface, or both. Add authenticat
30
31
  ```python
31
32
  import cycls
32
33
 
33
- agent = cycls.Agent(pip=["openai"])
34
+ cycls.api_key = "YOUR_CYCLS_API_KEY"
34
35
 
35
- @agent("my-agent", auth=True, analytics=True)
36
- async def chat(context):
36
+ @cycls.agent(pip=["openai"])
37
+ async def agent(context):
37
38
  from openai import AsyncOpenAI
38
39
  client = AsyncOpenAI()
39
40
 
40
- response = await client.chat.completions.create(
41
- model="gpt-4o",
42
- messages=context.messages,
43
- stream=True
41
+ stream = await client.responses.create(
42
+ model="o3-mini",
43
+ input=context.messages,
44
+ stream=True,
45
+ reasoning={"effort": "medium", "summary": "auto"},
44
46
  )
45
47
 
46
- async for chunk in response:
47
- if chunk.choices[0].delta.content:
48
- yield chunk.choices[0].delta.content
48
+ async for event in stream:
49
+ if event.type == "response.reasoning_summary_text.delta":
50
+ yield {"type": "thinking", "thinking": event.delta} # Renders as thinking bubble
51
+ elif event.type == "response.output_text.delta":
52
+ yield event.delta
49
53
 
50
- agent.deploy() # Live at https://my-agent.cycls.ai
54
+ agent.deploy() # Live at https://agent.cycls.ai
51
55
  ```
52
56
 
53
57
  ## Installation
@@ -64,7 +68,7 @@ Requires Docker.
64
68
  - **Web Interface** - Chat UI served automatically
65
69
  - **Authentication** - `auth=True` enables JWT-based access control
66
70
  - **Analytics** - `analytics=True` tracks usage
67
- - **Monetization** - `tier="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
71
+ - **Monetization** - `plan="cycls_pass"` integrates with [Cycls Pass](https://cycls.ai) subscriptions
68
72
  - **Native UI Components** - Render thinking bubbles, tables, code blocks in responses
69
73
 
70
74
  ## Running
@@ -72,17 +76,33 @@ Requires Docker.
72
76
  ```python
73
77
  agent.local() # Development with hot-reload (localhost:8080)
74
78
  agent.local(watch=False) # Development without hot-reload
75
- agent.deploy() # Production: https://agent-name.cycls.ai
79
+ agent.deploy() # Production: https://agent.cycls.ai
76
80
  ```
77
81
 
78
82
  Get an API key at [cycls.com](https://cycls.com).
79
83
 
84
+ ## Authentication & Analytics
85
+
86
+ ```python
87
+ @cycls.agent(pip=["openai"], auth=True, analytics=True)
88
+ async def agent(context):
89
+ # context.user available when auth=True
90
+ user = context.user # User(id, email, name, plans)
91
+ yield f"Hello {user.name}!"
92
+ ```
93
+
94
+ | Flag | Description |
95
+ |------|-------------|
96
+ | `auth=True` | Universal user pool via Cycls Pass (Clerk-based). You can also use your own Clerk auth. |
97
+ | `analytics=True` | Rich usage metrics available on the Cycls dashboard. |
98
+ | `plan="cycls_pass"` | Monetization via Cycls Pass subscriptions. Enables both auth and analytics. |
99
+
80
100
  ## Native UI Components
81
101
 
82
102
  Yield structured objects for rich streaming responses:
83
103
 
84
104
  ```python
85
- @agent()
105
+ @cycls.agent()
86
106
  async def demo(context):
87
107
  yield {"type": "thinking", "thinking": "Analyzing the request..."}
88
108
  yield "Here's what I found:\n\n"
@@ -105,32 +125,26 @@ async def demo(context):
105
125
  | `{"type": "callout", "callout": "...", "style": "..."}` | Yes |
106
126
  | `{"type": "image", "src": "..."}` | Yes |
107
127
 
108
- ### Reasoning Models
128
+ ### Thinking Bubbles
109
129
 
110
- ```python
111
- @agent()
112
- async def chat(context):
113
- from openai import AsyncOpenAI
114
- client = AsyncOpenAI()
130
+ 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
131
 
116
- stream = await client.responses.create(
117
- model="o3-mini",
118
- input=context.messages,
119
- stream=True,
120
- reasoning={"effort": "medium", "summary": "auto"},
121
- )
132
+ ```python
133
+ # Multiple yields build one thinking bubble
134
+ yield {"type": "thinking", "thinking": "Let me "}
135
+ yield {"type": "thinking", "thinking": "analyze this..."}
136
+ yield {"type": "thinking", "thinking": " Done thinking."}
122
137
 
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
138
+ # Then output the response
139
+ yield "Here's what I found..."
128
140
  ```
129
141
 
142
+ This works seamlessly with OpenAI's reasoning models - just map reasoning summaries to the thinking component.
143
+
130
144
  ## Context Object
131
145
 
132
146
  ```python
133
- @agent()
147
+ @cycls.agent()
134
148
  async def chat(context):
135
149
  context.messages # [{"role": "user", "content": "..."}]
136
150
  context.messages.raw # Full data including UI component parts
@@ -160,16 +174,17 @@ See [docs/streaming-protocol.md](docs/streaming-protocol.md) for frontend integr
160
174
 
161
175
  ## Declarative Infrastructure
162
176
 
163
- Define your entire runtime in Python:
177
+ Define your entire runtime in the decorator:
164
178
 
165
179
  ```python
166
- agent = cycls.Agent(
180
+ @cycls.agent(
167
181
  pip=["openai", "pandas", "numpy"],
168
182
  apt=["ffmpeg", "libmagic1"],
169
- run_commands=["curl -sSL https://example.com/setup.sh | bash"],
170
183
  copy=["./utils.py", "./models/", "/absolute/path/to/config.json"],
171
184
  copy_public=["./assets/logo.png", "./static/"],
172
185
  )
186
+ async def my_agent(context):
187
+ ...
173
188
  ```
174
189
 
175
190
  ### `pip` - Python Packages
@@ -188,17 +203,6 @@ Install system-level dependencies via apt-get. Need ffmpeg for audio processing?
188
203
  apt=["ffmpeg", "imagemagick", "libpq-dev"]
189
204
  ```
190
205
 
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
206
  ### `copy` - Bundle Files and Directories
203
207
 
204
208
  Include local files and directories in your container. Works with both relative and absolute paths. Copies files and entire directory trees.
@@ -214,7 +218,7 @@ copy=[
214
218
  Then import them in your function:
215
219
 
216
220
  ```python
217
- @agent()
221
+ @cycls.agent(copy=["./utils.py"])
218
222
  async def chat(context):
219
223
  from utils import helper_function # Your bundled module
220
224
  ...
@@ -0,0 +1,15 @@
1
+ from .sdk import function, agent
2
+ from .runtime import Runtime
3
+
4
+ def __getattr__(name):
5
+ from . import sdk
6
+ if name in ("api_key", "base_url"):
7
+ return getattr(sdk, name)
8
+ raise AttributeError(f"module 'cycls' has no attribute '{name}'")
9
+
10
+ def __setattr__(name, value):
11
+ from . import sdk
12
+ if name in ("api_key", "base_url"):
13
+ setattr(sdk, name, value)
14
+ return
15
+ raise AttributeError(f"module 'cycls' has no attribute '{name}'")
@@ -178,9 +178,11 @@ class Runtime:
178
178
 
179
179
  def _generate_dockerfile(self, port=None) -> str:
180
180
  """Generates a multi-stage Dockerfile string."""
181
- # Only install extra packages not in base image
181
+ using_base = self.base_image == BASE_IMAGE
182
+
183
+ # Only install extra packages not in base image (use uv if available in base)
182
184
  run_pip_install = (
183
- f"RUN pip install --no-cache-dir {' '.join(self.pip_packages)}"
185
+ f"RUN uv pip install --system --no-cache {' '.join(self.pip_packages)}"
184
186
  if self.pip_packages else ""
185
187
  )
186
188
  run_apt_install = (
@@ -191,16 +193,19 @@ class Runtime:
191
193
  copy_lines = "\n".join([f"COPY context_files/{dst} {dst}" for dst in self.copy.values()])
192
194
  expose_line = f"EXPOSE {port}" if port else ""
193
195
 
196
+ # Skip env/mkdir/workdir if using pre-built base (already configured)
197
+ env_lines = "" if using_base else f"""ENV PIP_ROOT_USER_ACTION=ignore \\
198
+ PYTHONUNBUFFERED=1
199
+ RUN mkdir -p {self.io_dir}
200
+ WORKDIR /app"""
201
+
194
202
  return f"""
195
203
  # STAGE 1: Base image with all dependencies
196
204
  FROM {self.base_image} as base
197
- ENV PIP_ROOT_USER_ACTION=ignore
198
- ENV PYTHONUNBUFFERED=1
199
- RUN mkdir -p {self.io_dir}
205
+ {env_lines}
200
206
  {run_apt_install}
201
207
  {run_pip_install}
202
208
  {run_shell_commands}
203
- WORKDIR /app
204
209
  {copy_lines}
205
210
  COPY {self.runner_filename} {self.runner_path}
206
211
  ENTRYPOINT ["python", "{self.runner_path}", "{self.io_dir}"]
@@ -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.71"
3
+ version = "0.0.2.73"
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