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.
@@ -0,0 +1,3 @@
1
+ from .deploy import generate_beamlit_deployment
2
+
3
+ __all__ = ["generate_beamlit_deployment"]
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: beamlit
3
- Version: 0.0.23rc13
3
+ Version: 0.0.23rc14
4
4
  Summary: Add your description here
5
5
  Author-email: cploujoux <ch.ploujoux@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -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.23rc13.dist-info/METADATA,sha256=pgkuT087O9yvCPPpcE64DjtxW5PcTcqBPA5oToxGwFk,2027
300
- beamlit-0.0.23rc13.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
301
- beamlit-0.0.23rc13.dist-info/RECORD,,
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,,