beamlit 0.0.23rc13__py3-none-any.whl → 0.0.23rc15__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.
@@ -65,6 +65,7 @@ class Settings(BaseSettings):
65
65
  name: str = Field(default="beamlit-agent")
66
66
  base_url: str = Field(default="https://api.beamlit.dev/v0")
67
67
  run_url: str = Field(default="https://run.beamlit.dev")
68
+ registry_url: str = Field(default="https://serverless-registry-production.beamlit.workers.dev")
68
69
  log_level: str = Field(default="INFO")
69
70
  agent: SettingsAgent = SettingsAgent()
70
71
  server: SettingsServer = SettingsServer()
@@ -0,0 +1,3 @@
1
+ from .deploy import generate_beamlit_deployment
2
+
3
+ __all__ = ["generate_beamlit_deployment"]
@@ -0,0 +1,503 @@
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, get_settings, init
11
+ from beamlit.models import (AgentChain, AgentDeployment, Flavor,
12
+ FunctionDeployment, Runtime,
13
+ StoreFunctionParameter)
14
+
15
+ sys.path.insert(0, os.getcwd())
16
+ sys.path.insert(0, os.path.join(os.getcwd(), "src"))
17
+
18
+
19
+ @dataclass
20
+ class Resource:
21
+ type: Literal["agent", "function"]
22
+ module: Callable
23
+ name: str
24
+ decorator: ast.Call
25
+ func: Callable
26
+
27
+ def get_resources(from_decorator, dir="src") -> list[Resource]:
28
+ """
29
+ Scans through Python files in a directory to find functions decorated with a specific decorator.
30
+
31
+ Args:
32
+ from_decorator (str): The name of the decorator to search for
33
+ dir (str): The directory to scan, defaults to "src"
34
+
35
+ Returns:
36
+ list[Resource]: List of Resource objects containing information about decorated functions
37
+ """
38
+ resources = []
39
+ logger = getLogger(__name__)
40
+
41
+ # Walk through all Python files in resources directory and subdirectories
42
+ for root, _, files in os.walk(dir):
43
+ for file in files:
44
+ if file.endswith(".py"):
45
+ file_path = os.path.join(root, file)
46
+ # Read and compile the file content
47
+ with open(file_path) as f:
48
+ try:
49
+ file_content = f.read()
50
+ # Parse the file content to find decorated resources
51
+ tree = ast.parse(file_content)
52
+
53
+ # Look for function definitions with decorators
54
+ for node in ast.walk(tree):
55
+ if (
56
+ not isinstance(node, ast.FunctionDef) and not isinstance(node, ast.AsyncFunctionDef)
57
+ ) or len(node.decorator_list) == 0:
58
+ continue
59
+ decorator = node.decorator_list[0]
60
+
61
+ decorator_name = ""
62
+ if isinstance(decorator, ast.Call):
63
+ decorator_name = decorator.func.id
64
+ if isinstance(decorator, ast.Name):
65
+ decorator_name = decorator.id
66
+ if decorator_name == from_decorator:
67
+ # Get the function name and decorator name
68
+ func_name = node.name
69
+
70
+ # Import the module to get the actual function
71
+ spec = importlib.util.spec_from_file_location(func_name, file_path)
72
+ module = importlib.util.module_from_spec(spec)
73
+ spec.loader.exec_module(module)
74
+ # Check if kit=True in the decorator arguments
75
+
76
+ # Get the decorated function
77
+ if hasattr(module, func_name) and isinstance(decorator, ast.Call):
78
+
79
+ resources.append(
80
+ Resource(
81
+ type=decorator_name,
82
+ module=module,
83
+ name=func_name,
84
+ func=getattr(module, func_name),
85
+ decorator=decorator,
86
+ )
87
+ )
88
+ except Exception as e:
89
+ logger.warning(f"Error processing {file_path}: {e!s}")
90
+ return resources
91
+
92
+
93
+ def get_parameters(resource: Resource) -> list[StoreFunctionParameter]:
94
+ """
95
+ Extracts parameter information from a function's signature and docstring.
96
+
97
+ Args:
98
+ resource (Resource): The resource object containing the function to analyze
99
+
100
+ Returns:
101
+ list[StoreFunctionParameter]: List of parameter objects with name, type, required status, and description
102
+ """
103
+ parameters = []
104
+ # Get function signature
105
+ import inspect
106
+ sig = inspect.signature(resource.func)
107
+ # Get docstring for parameter descriptions
108
+ docstring = inspect.getdoc(resource.func)
109
+ param_descriptions = {}
110
+ if docstring:
111
+ # Parse docstring for parameter descriptions
112
+ lines = docstring.split('\n')
113
+ for line in lines:
114
+ line = line.strip().lower()
115
+ if line.startswith(':param '):
116
+ # Extract parameter name and description
117
+ param_line = line[7:].split(':', 1)
118
+ if len(param_line) == 2:
119
+ param_name = param_line[0].strip()
120
+ param_desc = param_line[1].strip()
121
+ param_descriptions[param_name] = param_desc
122
+ for name, param in sig.parameters.items():
123
+ # Skip *args and **kwargs parameters
124
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
125
+
126
+ continue
127
+
128
+ param_type = "string" # Default type
129
+ type_mapping = {
130
+ 'str': 'string',
131
+ 'int': 'integer',
132
+ 'float': 'number',
133
+ 'bool': 'boolean',
134
+ 'list': 'array',
135
+ 'dict': 'object',
136
+ 'none': 'null'
137
+ }
138
+ if param.annotation != inspect.Parameter.empty:
139
+ # Map Python types to OpenAPI types
140
+ if hasattr(param.annotation, "__name__"):
141
+ param_type = param.annotation.__name__.lower()
142
+ else:
143
+ # Handle special types like Union, Optional etc
144
+ param_type = str(param.annotation).lower()
145
+ parameter = StoreFunctionParameter(
146
+ name=name,
147
+ type_=type_mapping.get(param_type, "string"),
148
+ required=param.default == inspect.Parameter.empty,
149
+ description=param_descriptions.get(name, f"Parameter {name}")
150
+ )
151
+ parameters.append(parameter)
152
+
153
+ return parameters
154
+
155
+
156
+ def get_description(description: str | None, resource: Resource) -> str:
157
+ """
158
+ Gets the description of a function from either a provided description or the function's docstring.
159
+
160
+ Args:
161
+ description (str | None): Optional explicit description
162
+ resource (Resource): The resource object containing the function
163
+
164
+ Returns:
165
+ str: The function description
166
+ """
167
+ if description:
168
+ return description
169
+ doc = resource.func.__doc__
170
+ if doc:
171
+ # Split docstring into sections and get only the description part
172
+ doc_lines = doc.split('\n')
173
+ description_lines = []
174
+ for line in doc_lines:
175
+ line = line.strip()
176
+ # Stop when we hit param/return sections
177
+ if line.startswith(':param') or line.startswith(':return'):
178
+ break
179
+ if line:
180
+ description_lines.append(line)
181
+ return ' '.join(description_lines).strip()
182
+ return ""
183
+
184
+ def get_kwargs(arg: ast.Call) -> dict:
185
+ """
186
+ Extracts keyword arguments from an AST Call node.
187
+
188
+ Args:
189
+ arg (ast.Call): The AST Call node to process
190
+
191
+ Returns:
192
+ dict: Dictionary of keyword arguments and their values
193
+ """
194
+ kwargs = {}
195
+ for keyword in arg.keywords:
196
+ if isinstance(keyword.value, ast.Constant):
197
+ kwargs[keyword.arg] = keyword.value.value
198
+ elif isinstance(keyword.value, (ast.List, ast.Tuple)):
199
+ kwargs[keyword.arg] = [
200
+ AgentChain(**get_kwargs(elem)) if isinstance(elem, ast.Call) and isinstance(elem.func, ast.Name) and elem.func.id == "AgentChain"
201
+ else elem.value if isinstance(elem, ast.Constant) else elem
202
+ for elem in keyword.value.elts
203
+ ]
204
+ elif isinstance(keyword.value, ast.Dict):
205
+ kwargs[keyword.arg] = {}
206
+ for k, v in zip(keyword.value.keys, keyword.value.values):
207
+ if isinstance(k, ast.Constant) and isinstance(v, ast.Constant):
208
+ kwargs[keyword.arg][k.value] = v.value
209
+ if isinstance(k, ast.Constant) and isinstance(v, ast.Call):
210
+ kwargs[keyword.arg][k.value] = get_kwargs(v)
211
+ return kwargs
212
+
213
+ def get_runtime(type: str, name: str) -> Runtime:
214
+ settings = get_settings()
215
+ registry_url = settings.registry_url.replace("https://", "").replace("http://", "")
216
+ image = f"{registry_url}/{settings.workspace}/{type}s/{name}"
217
+ return Runtime(image=image)
218
+
219
+ def get_beamlit_deployment_from_resource(resource: Resource) -> AgentDeployment | FunctionDeployment:
220
+ """
221
+ Creates a deployment configuration from a resource.
222
+
223
+ Args:
224
+ resource (Resource): The resource to create a deployment for
225
+
226
+ Returns:
227
+ AgentDeployment | FunctionDeployment: The deployment configuration
228
+ """
229
+ for arg in resource.decorator.args:
230
+ if isinstance(arg, ast.Call):
231
+ if isinstance(arg.func, ast.Name) and arg.func.id == "AgentDeployment":
232
+ kwargs = get_kwargs(arg)
233
+ description = kwargs.pop("description", None)
234
+ return AgentDeployment(
235
+ **kwargs,
236
+ description=get_description(description, resource),
237
+ runtime=get_runtime("agent", kwargs.get("agent", resource.name))
238
+ )
239
+ if isinstance(arg.func, ast.Name) and arg.func.id == "FunctionDeployment":
240
+ kwargs = get_kwargs(arg)
241
+ description = kwargs.pop("description", None)
242
+ return FunctionDeployment(
243
+ **kwargs,
244
+ parameters=get_parameters(resource),
245
+ description=get_description(description, resource),
246
+ runtime=get_runtime("function", kwargs.get("function", resource.name))
247
+ )
248
+ for arg in resource.decorator.keywords:
249
+ if isinstance(arg.value, ast.Call):
250
+ if isinstance(arg.value.func, ast.Name) and arg.value.func.id == "AgentDeployment":
251
+ kwargs = get_kwargs(arg.value)
252
+ description = kwargs.pop("description", None)
253
+ return AgentDeployment(
254
+ **kwargs,
255
+ description=get_description(description, resource),
256
+ runtime=get_runtime("agent", kwargs.get("agent", resource.name))
257
+ )
258
+ if isinstance(arg.value.func, ast.Name) and arg.value.func.id == "FunctionDeployment":
259
+ kwargs = get_kwargs(arg.value)
260
+ description = kwargs.pop("description", None)
261
+ return FunctionDeployment(
262
+ **kwargs,
263
+ parameters=get_parameters(resource),
264
+ description=get_description(description, resource),
265
+ runtime=get_runtime("function", kwargs.get("function", resource.name))
266
+ )
267
+ if resource.type == "agent":
268
+ return AgentDeployment(
269
+ agent=resource.name,
270
+ description=get_description(None,resource),
271
+ runtime=get_runtime("agent", resource.name)
272
+ )
273
+ if resource.type == "function":
274
+ return FunctionDeployment(
275
+ function=resource.name,
276
+ parameters=get_parameters(resource),
277
+ description=get_description(None,resource),
278
+ runtime=get_runtime("function", resource.name)
279
+ )
280
+ return None
281
+
282
+
283
+ def get_flavors(flavors: list[Flavor]) -> str:
284
+ """
285
+ Converts a list of Flavor objects to JSON string.
286
+
287
+ Args:
288
+ flavors (list[Flavor]): List of Flavor objects
289
+
290
+ Returns:
291
+ str: JSON string representation of flavors
292
+ """
293
+ if not flavors:
294
+ return "[]"
295
+ return json.dumps([flavor.to_dict() for flavor in flavors])
296
+
297
+ def format_parameters(parameters: list[StoreFunctionParameter]) -> str:
298
+ """
299
+ Formats function parameters into YAML-compatible string.
300
+
301
+ Args:
302
+ parameters (list[StoreFunctionParameter]): List of parameter objects
303
+
304
+ Returns:
305
+ str: YAML-formatted string of parameters
306
+ """
307
+ if not parameters:
308
+ return "[]"
309
+
310
+ formatted = []
311
+ for param in parameters:
312
+ formatted.append(f"""
313
+ - name: {param.name}
314
+ type: {param.type_}
315
+ required: {str(param.required).lower()}
316
+ description: {param.description}""")
317
+
318
+ return "\n".join(formatted)
319
+
320
+ def format_agent_chain(agent_chain: list[AgentChain]) -> str:
321
+ """
322
+ Formats agent chain configuration into YAML-compatible string.
323
+
324
+ Args:
325
+ agent_chain (list[AgentChain]): List of agent chain configurations
326
+
327
+ Returns:
328
+ str: YAML-formatted string of agent chain
329
+ """
330
+ if not agent_chain:
331
+ return "[]"
332
+ formatted = []
333
+
334
+ for agent in agent_chain:
335
+ formatted.append(f"""
336
+ - agent: {agent.name}
337
+ enabled: {agent.enabled}""")
338
+ if agent.description:
339
+ formatted.append(f" description: {agent.description}")
340
+ return "\n".join(formatted)
341
+
342
+ def get_agent_yaml(agent: AgentDeployment, functions: list[tuple[Resource, FunctionDeployment]], settings: Settings) -> str:
343
+ """
344
+ Generates YAML configuration for an agent deployment.
345
+
346
+ Args:
347
+ agent (AgentDeployment): Agent deployment configuration
348
+ functions (list[tuple[Resource, FunctionDeployment]]): List of associated functions
349
+ settings (Settings): Application settings
350
+
351
+ Returns:
352
+ str: YAML configuration string
353
+ """
354
+ template = f"""
355
+ apiVersion: beamlit.com/v1alpha1
356
+ kind: Agent
357
+ metadata:
358
+ name: {agent.agent}
359
+ spec:
360
+ display_name: {agent.agent}
361
+ deployments:
362
+ - environment: {settings.environment}
363
+ enabled: true
364
+ policies: [{", ".join(agent.policies or [])}]
365
+ functions: [{", ".join([f"{function.function}" for (_, function) in functions])}]
366
+ agent_chain: {format_agent_chain(agent.agent_chain)}
367
+ model: {agent.model}
368
+ runtime:
369
+ image: {agent.runtime.image}
370
+ """
371
+ if agent.description:
372
+ template += f""" description: |
373
+ {agent.description}"""
374
+ return template
375
+
376
+ def get_function_yaml(function: FunctionDeployment, settings: Settings) -> str:
377
+ """
378
+ Generates YAML configuration for a function deployment.
379
+
380
+ Args:
381
+ function (FunctionDeployment): Function deployment configuration
382
+ settings (Settings): Application settings
383
+
384
+ Returns:
385
+ str: YAML configuration string
386
+ """
387
+ return f"""
388
+ apiVersion: beamlit.com/v1alpha1
389
+ kind: Function
390
+ metadata:
391
+ name: {function.function}
392
+ spec:
393
+ display_name: {function.function}
394
+ deployments:
395
+ - environment: {settings.environment}
396
+ enabled: true
397
+ policies: [{", ".join(function.policies or [])}]
398
+ description: |
399
+ {function.description}
400
+ parameters: {format_parameters(function.parameters)}
401
+ runtime:
402
+ image: {function.runtime.image}
403
+ """
404
+
405
+ def dockerfile(type: Literal["agent", "function"], resource: Resource, deployment: AgentDeployment | FunctionDeployment) -> str:
406
+ """
407
+ Generates Dockerfile content for agent or function deployment.
408
+
409
+ Args:
410
+ type (Literal["agent", "function"]): Type of deployment
411
+ resource (Resource): Resource to be deployed
412
+ deployment (AgentDeployment | FunctionDeployment): Deployment configuration
413
+
414
+ Returns:
415
+ str: Dockerfile content
416
+ """
417
+ if type == "agent":
418
+ module = f"{resource.module.__file__.split('/')[-1].replace('.py', '')}.{resource.module.__name__}"
419
+ else:
420
+ module = f"functions.{resource.module.__name__}.{resource.func.__name__}"
421
+ cmd = ["bl", "serve", "--port", "80", "--module", module]
422
+ if type == "agent":
423
+ cmd.append("--remote")
424
+ cmd_str = ','.join([f'"{c}"' for c in cmd])
425
+
426
+ return f"""
427
+ FROM python:3.12-slim
428
+
429
+ ARG UV_VERSION="latest"
430
+ RUN apt update && apt install -y curl
431
+
432
+ # Install uv.
433
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
434
+ RUN curl -fsSL https://raw.githubusercontent.com/beamlit/toolkit/main/install.sh | BINDIR=/bin sh
435
+ WORKDIR /beamlit
436
+
437
+ # Install the application dependencies.
438
+ COPY pyproject.toml /beamlit/pyproject.toml
439
+ COPY uv.lock /beamlit/uv.lock
440
+ RUN uv sync --no-cache
441
+
442
+ COPY README.md /beamlit/README.md
443
+ COPY LICENSE /beamlit/LICENSE
444
+ COPY src /beamlit/src
445
+
446
+ ENV PATH="/beamlit/.venv/bin:$PATH"
447
+
448
+ ENTRYPOINT [{cmd_str}]
449
+ """
450
+
451
+ def generate_beamlit_deployment(directory: str):
452
+ """
453
+ Generates all necessary deployment files for Beamlit agents and functions.
454
+
455
+ Args:
456
+ directory (str): Target directory for generated files
457
+
458
+ Creates:
459
+ - Agent and function YAML configurations
460
+ - Dockerfiles for each deployment
461
+ - Directory structure for agents and functions
462
+ """
463
+ settings = init()
464
+ logger = getLogger(__name__)
465
+ logger.info(f"Importing server module: {settings.server.module}")
466
+ functions: list[tuple[Resource, FunctionDeployment]] = []
467
+ agents: list[tuple[Resource, AgentDeployment]] = []
468
+ for agent in get_resources("agent"):
469
+ agent_deployment = get_beamlit_deployment_from_resource(agent)
470
+ if agent_deployment:
471
+ agents.append((agent, agent_deployment))
472
+ for function in get_resources("function"):
473
+ function_deployment = get_beamlit_deployment_from_resource(function)
474
+ if function_deployment:
475
+ functions.append((function, function_deployment))
476
+
477
+ agents_dir = os.path.join(directory, "agents")
478
+ functions_dir = os.path.join(directory, "functions")
479
+ # Create directory if it doesn't exist
480
+ os.makedirs(agents_dir, exist_ok=True)
481
+ os.makedirs(functions_dir, exist_ok=True)
482
+ for (resource, agent) in agents:
483
+ # write deployment file
484
+ agent_dir = os.path.join(agents_dir, agent.agent)
485
+ os.makedirs(agent_dir, exist_ok=True)
486
+ with open(os.path.join(agent_dir, f"agent.yaml"), "w") as f:
487
+ content = get_agent_yaml(agent, functions, settings)
488
+ f.write(content)
489
+ # write dockerfile for build
490
+ with open(os.path.join(agent_dir, f"Dockerfile"), "w") as f:
491
+ content = dockerfile("agent", resource, agent)
492
+ f.write(content)
493
+ for (resource, function) in functions:
494
+ # write deployment file
495
+ function_dir = os.path.join(functions_dir, function.function)
496
+ os.makedirs(function_dir, exist_ok=True)
497
+ with open(os.path.join(function_dir, f"function.yaml"), "w") as f:
498
+ content = get_function_yaml(function, settings)
499
+ f.write(content)
500
+ # write dockerfile for build
501
+ with open(os.path.join(function_dir, f"Dockerfile"), "w") as f:
502
+ content = dockerfile("function", resource, function)
503
+ 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.23rc15
4
4
  Summary: Add your description here
5
5
  Author-email: cploujoux <ch.ploujoux@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -138,8 +138,10 @@ beamlit/common/__init__.py,sha256=yDoMJDKj-xjTGl7U1YI59KpWxiOV65HSiUulgO8xdTA,27
138
138
  beamlit/common/generate.py,sha256=VJ_MiRDulXdQdnlKdM4_Bod6CO6DOGlFiosGXOLuLGs,7227
139
139
  beamlit/common/logger.py,sha256=ayabnsoHS8ncXm8EpBS01FkvSe0XRcaNdQjKVuPI5z4,1025
140
140
  beamlit/common/secrets.py,sha256=sid81bOe3LflkMKDHwBsBs9nIju8bp5-v9qU9gkyNMc,212
141
- beamlit/common/settings.py,sha256=cL5HAg6atxnTJXL9Rxz5Xs4iApNCCYY5ijha8UM3PW4,5446
141
+ beamlit/common/settings.py,sha256=N0VzveNQ3vAE6ZJWMnWrIVi2hXidLtkRZkAXQTGo40A,5546
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=bWUXG_5b_XhFcG5t75vel2EbEfZPIdg_YLXtzkfgXUs,18799
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.23rc15.dist-info/METADATA,sha256=piLvTAjRvfpbIyBmUvCHSZHa2jxLezNRyfGLWmtIKBg,2027
302
+ beamlit-0.0.23rc15.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
303
+ beamlit-0.0.23rc15.dist-info/RECORD,,