beamlit 0.0.23rc13__py3-none-any.whl → 0.0.23rc14__py3-none-any.whl
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.
beamlit/deploy/deploy.py
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
import ast
|
2
|
+
import importlib
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from logging import getLogger
|
8
|
+
from typing import Callable, Literal
|
9
|
+
|
10
|
+
from beamlit.common.settings import Settings, init
|
11
|
+
from beamlit.models import (AgentChain, AgentDeployment, Flavor,
|
12
|
+
FunctionDeployment, StoreFunctionParameter)
|
13
|
+
|
14
|
+
sys.path.insert(0, os.getcwd())
|
15
|
+
sys.path.insert(0, os.path.join(os.getcwd(), "src"))
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Resource:
|
20
|
+
type: Literal["agent", "function"]
|
21
|
+
module: Callable
|
22
|
+
name: str
|
23
|
+
decorator: ast.Call
|
24
|
+
func: Callable
|
25
|
+
|
26
|
+
def get_resources(from_decorator, dir="src") -> list[Resource]:
|
27
|
+
resources = []
|
28
|
+
logger = getLogger(__name__)
|
29
|
+
|
30
|
+
# Walk through all Python files in resources directory and subdirectories
|
31
|
+
for root, _, files in os.walk(dir):
|
32
|
+
for file in files:
|
33
|
+
if file.endswith(".py"):
|
34
|
+
file_path = os.path.join(root, file)
|
35
|
+
# Read and compile the file content
|
36
|
+
with open(file_path) as f:
|
37
|
+
try:
|
38
|
+
file_content = f.read()
|
39
|
+
# Parse the file content to find decorated resources
|
40
|
+
tree = ast.parse(file_content)
|
41
|
+
|
42
|
+
# Look for function definitions with decorators
|
43
|
+
for node in ast.walk(tree):
|
44
|
+
if (
|
45
|
+
not isinstance(node, ast.FunctionDef) and not isinstance(node, ast.AsyncFunctionDef)
|
46
|
+
) or len(node.decorator_list) == 0:
|
47
|
+
continue
|
48
|
+
decorator = node.decorator_list[0]
|
49
|
+
|
50
|
+
decorator_name = ""
|
51
|
+
if isinstance(decorator, ast.Call):
|
52
|
+
decorator_name = decorator.func.id
|
53
|
+
if isinstance(decorator, ast.Name):
|
54
|
+
decorator_name = decorator.id
|
55
|
+
if decorator_name == from_decorator:
|
56
|
+
# Get the function name and decorator name
|
57
|
+
func_name = node.name
|
58
|
+
|
59
|
+
# Import the module to get the actual function
|
60
|
+
spec = importlib.util.spec_from_file_location(func_name, file_path)
|
61
|
+
module = importlib.util.module_from_spec(spec)
|
62
|
+
spec.loader.exec_module(module)
|
63
|
+
# Check if kit=True in the decorator arguments
|
64
|
+
|
65
|
+
# Get the decorated function
|
66
|
+
if hasattr(module, func_name) and isinstance(decorator, ast.Call):
|
67
|
+
|
68
|
+
resources.append(
|
69
|
+
Resource(
|
70
|
+
type=decorator_name,
|
71
|
+
module=module,
|
72
|
+
name=func_name,
|
73
|
+
func=getattr(module, func_name),
|
74
|
+
decorator=decorator,
|
75
|
+
)
|
76
|
+
)
|
77
|
+
except Exception as e:
|
78
|
+
logger.warning(f"Error processing {file_path}: {e!s}")
|
79
|
+
return resources
|
80
|
+
|
81
|
+
|
82
|
+
def get_parameters(resource: Resource) -> list[StoreFunctionParameter]:
|
83
|
+
parameters = []
|
84
|
+
# Get function signature
|
85
|
+
import inspect
|
86
|
+
sig = inspect.signature(resource.func)
|
87
|
+
# Get docstring for parameter descriptions
|
88
|
+
docstring = inspect.getdoc(resource.func)
|
89
|
+
param_descriptions = {}
|
90
|
+
if docstring:
|
91
|
+
# Parse docstring for parameter descriptions
|
92
|
+
lines = docstring.split('\n')
|
93
|
+
for line in lines:
|
94
|
+
line = line.strip().lower()
|
95
|
+
if line.startswith(':param '):
|
96
|
+
# Extract parameter name and description
|
97
|
+
param_line = line[7:].split(':', 1)
|
98
|
+
if len(param_line) == 2:
|
99
|
+
param_name = param_line[0].strip()
|
100
|
+
param_desc = param_line[1].strip()
|
101
|
+
param_descriptions[param_name] = param_desc
|
102
|
+
for name, param in sig.parameters.items():
|
103
|
+
# Skip *args and **kwargs parameters
|
104
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
105
|
+
|
106
|
+
continue
|
107
|
+
|
108
|
+
param_type = "string" # Default type
|
109
|
+
type_mapping = {
|
110
|
+
'str': 'string',
|
111
|
+
'int': 'integer',
|
112
|
+
'float': 'number',
|
113
|
+
'bool': 'boolean',
|
114
|
+
'list': 'array',
|
115
|
+
'dict': 'object',
|
116
|
+
'none': 'null'
|
117
|
+
}
|
118
|
+
if param.annotation != inspect.Parameter.empty:
|
119
|
+
# Map Python types to OpenAPI types
|
120
|
+
if hasattr(param.annotation, "__name__"):
|
121
|
+
param_type = param.annotation.__name__.lower()
|
122
|
+
else:
|
123
|
+
# Handle special types like Union, Optional etc
|
124
|
+
param_type = str(param.annotation).lower()
|
125
|
+
parameter = StoreFunctionParameter(
|
126
|
+
name=name,
|
127
|
+
type_=type_mapping.get(param_type, "string"),
|
128
|
+
required=param.default == inspect.Parameter.empty,
|
129
|
+
description=param_descriptions.get(name, f"Parameter {name}")
|
130
|
+
)
|
131
|
+
parameters.append(parameter)
|
132
|
+
|
133
|
+
return parameters
|
134
|
+
|
135
|
+
|
136
|
+
def get_description(description: str | None, resource: Resource) -> str:
|
137
|
+
if description:
|
138
|
+
return description
|
139
|
+
doc = resource.func.__doc__
|
140
|
+
if doc:
|
141
|
+
# Split docstring into sections and get only the description part
|
142
|
+
doc_lines = doc.split('\n')
|
143
|
+
description_lines = []
|
144
|
+
for line in doc_lines:
|
145
|
+
line = line.strip()
|
146
|
+
# Stop when we hit param/return sections
|
147
|
+
if line.startswith(':param') or line.startswith(':return'):
|
148
|
+
break
|
149
|
+
if line:
|
150
|
+
description_lines.append(line)
|
151
|
+
return ' '.join(description_lines).strip()
|
152
|
+
return ""
|
153
|
+
|
154
|
+
def get_kwargs(arg: ast.Call) -> dict:
|
155
|
+
kwargs = {}
|
156
|
+
for keyword in arg.keywords:
|
157
|
+
if isinstance(keyword.value, ast.Constant):
|
158
|
+
kwargs[keyword.arg] = keyword.value.value
|
159
|
+
elif isinstance(keyword.value, (ast.List, ast.Tuple)):
|
160
|
+
kwargs[keyword.arg] = [
|
161
|
+
AgentChain(**get_kwargs(elem)) if isinstance(elem, ast.Call) and isinstance(elem.func, ast.Name) and elem.func.id == "AgentChain"
|
162
|
+
else elem.value if isinstance(elem, ast.Constant) else elem
|
163
|
+
for elem in keyword.value.elts
|
164
|
+
]
|
165
|
+
elif isinstance(keyword.value, ast.Dict):
|
166
|
+
kwargs[keyword.arg] = {}
|
167
|
+
for k, v in zip(keyword.value.keys, keyword.value.values):
|
168
|
+
if isinstance(k, ast.Constant) and isinstance(v, ast.Constant):
|
169
|
+
kwargs[keyword.arg][k.value] = v.value
|
170
|
+
if isinstance(k, ast.Constant) and isinstance(v, ast.Call):
|
171
|
+
kwargs[keyword.arg][k.value] = get_kwargs(v)
|
172
|
+
return kwargs
|
173
|
+
|
174
|
+
def get_beamlit_deployment_from_resource(resource: Resource) -> AgentDeployment | FunctionDeployment:
|
175
|
+
for arg in resource.decorator.args:
|
176
|
+
if isinstance(arg, ast.Call):
|
177
|
+
if isinstance(arg.func, ast.Name) and arg.func.id == "AgentDeployment":
|
178
|
+
kwargs = get_kwargs(arg)
|
179
|
+
description = kwargs.pop("description", None)
|
180
|
+
return AgentDeployment(**kwargs, description=get_description(description, resource))
|
181
|
+
if isinstance(arg.func, ast.Name) and arg.func.id == "FunctionDeployment":
|
182
|
+
kwargs = get_kwargs(arg)
|
183
|
+
description = kwargs.pop("description", None)
|
184
|
+
return FunctionDeployment(**kwargs, parameters=get_parameters(resource), description=get_description(description, resource))
|
185
|
+
if resource.type == "agent":
|
186
|
+
return AgentDeployment(
|
187
|
+
agent=resource.name,
|
188
|
+
description=get_description(None,resource)
|
189
|
+
)
|
190
|
+
if resource.type == "function":
|
191
|
+
return FunctionDeployment(
|
192
|
+
function=resource.name,
|
193
|
+
parameters=get_parameters(resource),
|
194
|
+
description=get_description(None,resource)
|
195
|
+
)
|
196
|
+
return None
|
197
|
+
|
198
|
+
|
199
|
+
def get_flavors(flavors: list[Flavor]) -> str:
|
200
|
+
if not flavors:
|
201
|
+
return "[]"
|
202
|
+
return json.dumps([flavor.to_dict() for flavor in flavors])
|
203
|
+
|
204
|
+
def format_parameters(parameters: list[StoreFunctionParameter]) -> str:
|
205
|
+
if not parameters:
|
206
|
+
return "[]"
|
207
|
+
|
208
|
+
formatted = []
|
209
|
+
for param in parameters:
|
210
|
+
formatted.append(f"""
|
211
|
+
- name: {param.name}
|
212
|
+
type: {param.type_}
|
213
|
+
required: {str(param.required).lower()}
|
214
|
+
description: {param.description}""")
|
215
|
+
|
216
|
+
return "\n".join(formatted)
|
217
|
+
|
218
|
+
def format_agent_chain(agent_chain: list[AgentChain]) -> str:
|
219
|
+
if not agent_chain:
|
220
|
+
return "[]"
|
221
|
+
formatted = []
|
222
|
+
|
223
|
+
for agent in agent_chain:
|
224
|
+
formatted.append(f"""
|
225
|
+
- agent: {agent.name}
|
226
|
+
enabled: {agent.enabled}""")
|
227
|
+
if agent.description:
|
228
|
+
formatted.append(f" description: {agent.description}")
|
229
|
+
return "\n".join(formatted)
|
230
|
+
|
231
|
+
def get_agent_yaml(agent: AgentDeployment, functions: list[tuple[Resource, FunctionDeployment]], settings: Settings) -> str:
|
232
|
+
template = f"""
|
233
|
+
apiVersion: beamlit.com/v1alpha1
|
234
|
+
kind: Agent
|
235
|
+
metadata:
|
236
|
+
name: {agent.agent}
|
237
|
+
spec:
|
238
|
+
display_name: Agent - {agent.agent}
|
239
|
+
deployments:
|
240
|
+
- environment: production
|
241
|
+
enabled: true
|
242
|
+
policies: [{", ".join(agent.policies or [])}]
|
243
|
+
functions: [{", ".join([f"{function.function}" for (_, function) in functions])}]
|
244
|
+
agent_chain: {format_agent_chain(agent.agent_chain)}
|
245
|
+
model: {agent.model}
|
246
|
+
"""
|
247
|
+
if agent.description:
|
248
|
+
template += f""" description: |
|
249
|
+
{agent.description}"""
|
250
|
+
return template
|
251
|
+
|
252
|
+
def get_function_yaml(function: FunctionDeployment, settings: Settings) -> str:
|
253
|
+
return f"""
|
254
|
+
apiVersion: beamlit.com/v1alpha1
|
255
|
+
kind: Function
|
256
|
+
metadata:
|
257
|
+
name: {function.function}
|
258
|
+
spec:
|
259
|
+
display_name: {function.function}
|
260
|
+
deployments:
|
261
|
+
- environment: {settings.environment}
|
262
|
+
enabled: true
|
263
|
+
policies: [{", ".join(function.policies or [])}]
|
264
|
+
description: |
|
265
|
+
{function.description}
|
266
|
+
parameters: {format_parameters(function.parameters)}
|
267
|
+
"""
|
268
|
+
|
269
|
+
def dockerfile(type: Literal["agent", "function"], resource: Resource, deployment: AgentDeployment | FunctionDeployment):
|
270
|
+
if type == "agent":
|
271
|
+
module = f"{resource.module.__file__.split('/')[-1].replace('.py', '')}.{resource.module.__name__}"
|
272
|
+
else:
|
273
|
+
module = f"functions.{resource.module.__name__}.{resource.func.__name__}"
|
274
|
+
cmd = ["bl", "serve", "--port", "80", "--module", module]
|
275
|
+
if type == "agent":
|
276
|
+
cmd.append("--remote")
|
277
|
+
cmd_str = ','.join([f'"{c}"' for c in cmd])
|
278
|
+
|
279
|
+
return f"""
|
280
|
+
FROM python:3.12-slim
|
281
|
+
|
282
|
+
ARG UV_VERSION="latest"
|
283
|
+
RUN apt update && apt install -y curl
|
284
|
+
|
285
|
+
# Install uv.
|
286
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
287
|
+
RUN curl -fsSL https://raw.githubusercontent.com/beamlit/toolkit/main/install.sh | BINDIR=/bin sh
|
288
|
+
WORKDIR /beamlit
|
289
|
+
|
290
|
+
# Install the application dependencies.
|
291
|
+
COPY pyproject.toml /beamlit/pyproject.toml
|
292
|
+
COPY uv.lock /beamlit/uv.lock
|
293
|
+
RUN uv sync --no-cache
|
294
|
+
|
295
|
+
COPY README.md /beamlit/README.md
|
296
|
+
COPY LICENSE /beamlit/LICENSE
|
297
|
+
COPY src /beamlit/src
|
298
|
+
|
299
|
+
ENV PATH="/beamlit/.venv/bin:$PATH"
|
300
|
+
|
301
|
+
ENTRYPOINT [{cmd_str}]
|
302
|
+
"""
|
303
|
+
|
304
|
+
def generate_beamlit_deployment(directory: str):
|
305
|
+
settings = init()
|
306
|
+
logger = getLogger(__name__)
|
307
|
+
logger.info(f"Importing server module: {settings.server.module}")
|
308
|
+
functions: list[tuple[Resource, FunctionDeployment]] = []
|
309
|
+
agents: list[tuple[Resource, AgentDeployment]] = []
|
310
|
+
for agent in get_resources("agent"):
|
311
|
+
agent_deployment = get_beamlit_deployment_from_resource(agent)
|
312
|
+
if agent_deployment:
|
313
|
+
agents.append((agent, agent_deployment))
|
314
|
+
for function in get_resources("function"):
|
315
|
+
function_deployment = get_beamlit_deployment_from_resource(function)
|
316
|
+
if function_deployment:
|
317
|
+
functions.append((function, function_deployment))
|
318
|
+
|
319
|
+
agents_dir = os.path.join(directory, "agents")
|
320
|
+
functions_dir = os.path.join(directory, "functions")
|
321
|
+
# Create directory if it doesn't exist
|
322
|
+
os.makedirs(agents_dir, exist_ok=True)
|
323
|
+
os.makedirs(functions_dir, exist_ok=True)
|
324
|
+
for (resource, agent) in agents:
|
325
|
+
# write deployment file
|
326
|
+
agent_dir = os.path.join(agents_dir, agent.agent)
|
327
|
+
os.makedirs(agent_dir, exist_ok=True)
|
328
|
+
with open(os.path.join(agent_dir, f"agent.yaml"), "w") as f:
|
329
|
+
content = get_agent_yaml(agent, functions, settings)
|
330
|
+
f.write(content)
|
331
|
+
# write dockerfile for build
|
332
|
+
with open(os.path.join(agent_dir, f"Dockerfile"), "w") as f:
|
333
|
+
content = dockerfile("agent", resource, agent)
|
334
|
+
f.write(content)
|
335
|
+
for (resource, function) in functions:
|
336
|
+
# write deployment file
|
337
|
+
function_dir = os.path.join(functions_dir, function.function)
|
338
|
+
os.makedirs(function_dir, exist_ok=True)
|
339
|
+
with open(os.path.join(function_dir, f"function.yaml"), "w") as f:
|
340
|
+
content = get_function_yaml(function, settings)
|
341
|
+
f.write(content)
|
342
|
+
# write dockerfile for build
|
343
|
+
with open(os.path.join(function_dir, f"Dockerfile"), "w") as f:
|
344
|
+
content = dockerfile("function", resource, function)
|
345
|
+
f.write(content)
|
@@ -140,6 +140,8 @@ beamlit/common/logger.py,sha256=ayabnsoHS8ncXm8EpBS01FkvSe0XRcaNdQjKVuPI5z4,1025
|
|
140
140
|
beamlit/common/secrets.py,sha256=sid81bOe3LflkMKDHwBsBs9nIju8bp5-v9qU9gkyNMc,212
|
141
141
|
beamlit/common/settings.py,sha256=cL5HAg6atxnTJXL9Rxz5Xs4iApNCCYY5ijha8UM3PW4,5446
|
142
142
|
beamlit/common/utils.py,sha256=jouz5igBvT37Xn_e94-foCHyQczVim-UzVcoIF6RWJ4,657
|
143
|
+
beamlit/deploy/__init__.py,sha256=GS7l7Jtm2yKs7iNLKcfjYO-rAhUzggQ3xiYSf3oxLBY,91
|
144
|
+
beamlit/deploy/deploy.py,sha256=F79JRGhZV-5wIvux66UC6ATSnqQkTfL2MvD8ayVy6-o,13487
|
143
145
|
beamlit/functions/__init__.py,sha256=_RPG1Bfg54JGdIPnViAU6n9zD7E1cDNsdXi8oYGskzE,138
|
144
146
|
beamlit/functions/decorator.py,sha256=uYZOVxD-7ZNHORTQfCIn0qdNKPIZupsr7_QdUmWEKu0,2996
|
145
147
|
beamlit/functions/github/__init__.py,sha256=gYnUkeegukOfbymdabuuJkScvH-_ZJygX05BoqkPn0o,49
|
@@ -296,6 +298,6 @@ beamlit/serve/app.py,sha256=_0ZesKcczd1sYm8vs3ulbXO1M1boO_5DhFf3jSmjM4g,2398
|
|
296
298
|
beamlit/serve/middlewares/__init__.py,sha256=1dVmnOmhAQWvWktqHkKSIX-YoF6fmMU8xkUQuhg_rJU,148
|
297
299
|
beamlit/serve/middlewares/accesslog.py,sha256=wM52-hcwtO-_hdM1pnsEJzerzJf1MzEyN5m85BdDccE,609
|
298
300
|
beamlit/serve/middlewares/processtime.py,sha256=lDAaIasZ4bwvN-HKHvZpaD9r-yrkVNZYx4abvbjbrCg,411
|
299
|
-
beamlit-0.0.
|
300
|
-
beamlit-0.0.
|
301
|
-
beamlit-0.0.
|
301
|
+
beamlit-0.0.23rc14.dist-info/METADATA,sha256=l7YaLMfPwUhKPfEiD2-Ve1OtVt6U3dkdR9rEh243w5Y,2027
|
302
|
+
beamlit-0.0.23rc14.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
303
|
+
beamlit-0.0.23rc14.dist-info/RECORD,,
|
File without changes
|