mlbricks-studio 1.0.0b1__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.
- mlb_studio/__init__.py +5 -0
- mlb_studio/api_graph_runtime.py +347 -0
- mlb_studio/api_registry.py +229 -0
- mlb_studio/builder.py +3808 -0
- mlb_studio/cloud.py +581 -0
- mlb_studio/data.py +918 -0
- mlb_studio/design_io.py +31 -0
- mlb_studio/elasticbit_runtime.py +112 -0
- mlb_studio/graph.py +950 -0
- mlb_studio/hub.py +400 -0
- mlb_studio/import_pool.py +461 -0
- mlb_studio/local_runtime.py +358 -0
- mlb_studio/mlbricks_api_schema.json +2702 -0
- mlb_studio/model_runtime.py +1965 -0
- mlb_studio/persistence.py +479 -0
- mlb_studio/runner.py +455 -0
- mlb_studio/runtime.py +37 -0
- mlb_studio/security.py +130 -0
- mlb_studio/serve.py +483 -0
- mlb_studio/static/builder.css +3698 -0
- mlb_studio/static/builder.js +9825 -0
- mlb_studio/static/favicon-32.png +0 -0
- mlb_studio/static/favicon.ico +0 -0
- mlb_studio/static/favicon.png +0 -0
- mlb_studio/static/favicon.svg +1 -0
- mlb_studio/version.py +3 -0
- mlbricks_studio-1.0.0b1.dist-info/METADATA +1978 -0
- mlbricks_studio-1.0.0b1.dist-info/RECORD +34 -0
- mlbricks_studio-1.0.0b1.dist-info/WHEEL +5 -0
- mlbricks_studio-1.0.0b1.dist-info/entry_points.txt +3 -0
- mlbricks_studio-1.0.0b1.dist-info/licenses/LICENSE +9 -0
- mlbricks_studio-1.0.0b1.dist-info/top_level.txt +2 -0
- mlbstudio/__init__.py +6 -0
- mlbstudio/cli.py +14 -0
mlb_studio/__init__.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Declarative runtime contracts for MLBricks components.
|
|
4
|
+
|
|
5
|
+
Studio is an orchestrator: contracts describe how a visual node maps to the
|
|
6
|
+
original MLBricks constructor and forward API. The graph executor itself does
|
|
7
|
+
not need per-component branches.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Mapping
|
|
15
|
+
|
|
16
|
+
from .import_pool import IMPORT_POOL
|
|
17
|
+
|
|
18
|
+
_SCHEMA_PATH = Path(__file__).with_name("mlbricks_api_schema.json")
|
|
19
|
+
_SCHEMA_CACHE: dict[str, Any] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _schema() -> dict[str, Any]:
|
|
23
|
+
global _SCHEMA_CACHE
|
|
24
|
+
if _SCHEMA_CACHE is None:
|
|
25
|
+
try:
|
|
26
|
+
_SCHEMA_CACHE = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
27
|
+
except Exception:
|
|
28
|
+
_SCHEMA_CACHE = {}
|
|
29
|
+
return _SCHEMA_CACHE or {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _bool(value: Any) -> bool:
|
|
33
|
+
if isinstance(value, bool):
|
|
34
|
+
return value
|
|
35
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _coerce(value: Any, annotation: str | None) -> Any:
|
|
39
|
+
kind = str(annotation or "").lower()
|
|
40
|
+
if value is None:
|
|
41
|
+
return None
|
|
42
|
+
if "bool" in kind:
|
|
43
|
+
return _bool(value)
|
|
44
|
+
if "int" in kind:
|
|
45
|
+
return int(float(value))
|
|
46
|
+
if "float" in kind:
|
|
47
|
+
return float(value)
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class APIComponentContract:
|
|
53
|
+
"""Mapping between one Studio node and an original MLBricks API.
|
|
54
|
+
|
|
55
|
+
``input_ports`` maps Studio input-port names to forward argument names.
|
|
56
|
+
``output_ports`` maps Studio output-port names to result positions/keys.
|
|
57
|
+
For the common one-input/one-output case no custom code is required.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
component_type: str
|
|
61
|
+
import_key: str
|
|
62
|
+
input_ports: Mapping[str, str]
|
|
63
|
+
output_ports: Mapping[str, Any]
|
|
64
|
+
parameter_aliases: Mapping[str, tuple[str, ...]] = None
|
|
65
|
+
runtime_sources: Mapping[str, str] = None
|
|
66
|
+
input_initializers: Mapping[str, Mapping[str, Any]] = None
|
|
67
|
+
|
|
68
|
+
def _parameter_value(self, key: str, spec: dict[str, Any], params: dict[str, Any], runtime: dict[str, Any]):
|
|
69
|
+
aliases = (self.parameter_aliases or {}).get(key, ())
|
|
70
|
+
candidates = (key, *aliases)
|
|
71
|
+
for candidate in candidates:
|
|
72
|
+
if candidate in params and params[candidate] not in (None, ""):
|
|
73
|
+
return params[candidate], True
|
|
74
|
+
runtime_key = (self.runtime_sources or {}).get(key)
|
|
75
|
+
if runtime_key and runtime.get(runtime_key) not in (None, ""):
|
|
76
|
+
return runtime[runtime_key], True
|
|
77
|
+
if spec.get("value") is not None:
|
|
78
|
+
return spec.get("value"), True
|
|
79
|
+
if spec.get("default") is not None:
|
|
80
|
+
return spec.get("default"), True
|
|
81
|
+
return None, False
|
|
82
|
+
|
|
83
|
+
def constructor_kwargs(self, node: dict[str, Any], runtime: dict[str, Any]) -> dict[str, Any]:
|
|
84
|
+
params = deepcopy(node.get("params") or {})
|
|
85
|
+
component_schema = (_schema().get("components") or _schema()).get(self.component_type) or {}
|
|
86
|
+
kwargs: dict[str, Any] = {}
|
|
87
|
+
for spec in component_schema.get("parameters") or []:
|
|
88
|
+
key = str(spec.get("key") or spec.get("name") or "").strip()
|
|
89
|
+
if not key:
|
|
90
|
+
continue
|
|
91
|
+
value, present = self._parameter_value(key, spec, params, runtime)
|
|
92
|
+
if not present:
|
|
93
|
+
if spec.get("required"):
|
|
94
|
+
raise ValueError(f"{node.get('name', self.component_type)} requires {key}.")
|
|
95
|
+
continue
|
|
96
|
+
# UI/runtime values override constructor defaults, but 'auto' backend
|
|
97
|
+
# is intentionally preserved because it is part of the original API.
|
|
98
|
+
kwargs[key] = _coerce(value, spec.get("annotation") or spec.get("type"))
|
|
99
|
+
return kwargs
|
|
100
|
+
|
|
101
|
+
def instantiate(self, node: dict[str, Any], runtime: dict[str, Any]):
|
|
102
|
+
cls = IMPORT_POOL.resolve_component(self.import_key)
|
|
103
|
+
return cls(**self.constructor_kwargs(node, runtime))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def initialize_input(self, port: str, inputs: Mapping[str, Any], node: Mapping[str, Any], module):
|
|
107
|
+
"""Create a declarative default for an unconnected logical input.
|
|
108
|
+
|
|
109
|
+
This keeps first-depth initialization in the component contract instead
|
|
110
|
+
of adding component-specific branches to the graph executor. Supported
|
|
111
|
+
initializers are intentionally small and tensor-shape driven.
|
|
112
|
+
"""
|
|
113
|
+
spec = dict((self.input_initializers or {}).get(port) or {})
|
|
114
|
+
if not spec:
|
|
115
|
+
return None, False
|
|
116
|
+
|
|
117
|
+
source_name = str(spec.get("source") or "")
|
|
118
|
+
source = inputs.get(source_name)
|
|
119
|
+
if source is None:
|
|
120
|
+
return None, False
|
|
121
|
+
|
|
122
|
+
kind = str(spec.get("kind") or "").lower()
|
|
123
|
+
if kind == "zeros_like":
|
|
124
|
+
return source.new_zeros(source.shape), True
|
|
125
|
+
|
|
126
|
+
if kind == "module_method":
|
|
127
|
+
method_name = str(spec.get("method") or "")
|
|
128
|
+
method = getattr(module, method_name, None)
|
|
129
|
+
if not callable(method):
|
|
130
|
+
return None, False
|
|
131
|
+
return method(source), True
|
|
132
|
+
|
|
133
|
+
if kind == "zeros_feature":
|
|
134
|
+
width = None
|
|
135
|
+
width_attr = str(spec.get("width_attr") or "")
|
|
136
|
+
if width_attr and hasattr(module, width_attr):
|
|
137
|
+
width = getattr(module, width_attr)
|
|
138
|
+
if width in (None, ""):
|
|
139
|
+
width_param = str(spec.get("width_param") or "")
|
|
140
|
+
if width_param:
|
|
141
|
+
width = (node.get("params") or {}).get(width_param)
|
|
142
|
+
if width in (None, ""):
|
|
143
|
+
return None, False
|
|
144
|
+
shape = (*source.shape[:-1], int(width))
|
|
145
|
+
return source.new_zeros(shape), True
|
|
146
|
+
|
|
147
|
+
raise ValueError(f"Unsupported input initializer kind {kind!r} for {self.component_type}.{port}.")
|
|
148
|
+
|
|
149
|
+
def execute(self, module, inputs: Mapping[str, Any]):
|
|
150
|
+
kwargs = {
|
|
151
|
+
api_arg: inputs[port]
|
|
152
|
+
for port, api_arg in self.input_ports.items()
|
|
153
|
+
if port in inputs
|
|
154
|
+
}
|
|
155
|
+
result = module(**kwargs)
|
|
156
|
+
if len(self.output_ports) == 1 and "main" in self.output_ports:
|
|
157
|
+
selector = self.output_ports["main"]
|
|
158
|
+
if selector is None:
|
|
159
|
+
return {"main": result}
|
|
160
|
+
outputs: dict[str, Any] = {}
|
|
161
|
+
for port, selector in self.output_ports.items():
|
|
162
|
+
if selector is None:
|
|
163
|
+
outputs[port] = result
|
|
164
|
+
elif isinstance(selector, int):
|
|
165
|
+
outputs[port] = result[selector]
|
|
166
|
+
else:
|
|
167
|
+
outputs[port] = result[selector] if isinstance(result, dict) else getattr(result, str(selector))
|
|
168
|
+
return outputs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class VisionClassificationContract(APIComponentContract):
|
|
172
|
+
# TensorGraph contract for image-classification vision engines.
|
|
173
|
+
_CLASSIFIER_ENGINES = {"serpentine", "vit", "visiontransformer", "cnn"}
|
|
174
|
+
|
|
175
|
+
def constructor_kwargs(self, node, runtime):
|
|
176
|
+
kwargs = super().constructor_kwargs(node, runtime)
|
|
177
|
+
engine = str(kwargs.get("engine") or "Serpentine").strip()
|
|
178
|
+
key = engine.lower().replace("-", "").replace("_", "").replace(" ", "")
|
|
179
|
+
if key not in self._CLASSIFIER_ENGINES:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"{node.get('name', self.component_type)} engine={engine!r} is "
|
|
182
|
+
"not an image-classification TensorGraph mode. Studio currently "
|
|
183
|
+
"supports Serpentine, ViT/VisionTransformer and CNN here; "
|
|
184
|
+
"Diffusion requires timesteps and AR requires visual token IDs."
|
|
185
|
+
)
|
|
186
|
+
return kwargs
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class APIComponentRegistry:
|
|
190
|
+
def __init__(self):
|
|
191
|
+
self._contracts: dict[str, APIComponentContract] = {}
|
|
192
|
+
|
|
193
|
+
def register(self, contract: APIComponentContract) -> APIComponentContract:
|
|
194
|
+
self._contracts[contract.component_type] = contract
|
|
195
|
+
return contract
|
|
196
|
+
|
|
197
|
+
def get(self, component_type: str | None) -> APIComponentContract | None:
|
|
198
|
+
return self._contracts.get(str(component_type or ""))
|
|
199
|
+
|
|
200
|
+
def __contains__(self, component_type: str) -> bool:
|
|
201
|
+
return component_type in self._contracts
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
API_COMPONENTS = APIComponentRegistry()
|
|
205
|
+
|
|
206
|
+
# RoPE is a standard one-input/one-output MLBricks module. The original API
|
|
207
|
+
# expects a Q/K-like tensor [B,H,T,D]; start_pos defaults to zero in MLBricks,
|
|
208
|
+
# so Studio can use the universal execution path without a TensorGraph branch.
|
|
209
|
+
API_COMPONENTS.register(APIComponentContract(
|
|
210
|
+
component_type="rope",
|
|
211
|
+
import_key="rope",
|
|
212
|
+
input_ports={"main": "x"},
|
|
213
|
+
output_ports={"main": None},
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
# First proof of the universal path. No BOLT-specific branch is required in
|
|
217
|
+
# TensorGraph: this declaration maps the visual node to Bolt's original API.
|
|
218
|
+
API_COMPONENTS.register(APIComponentContract(
|
|
219
|
+
component_type="bolt",
|
|
220
|
+
import_key="bolt",
|
|
221
|
+
input_ports={"main": "x"},
|
|
222
|
+
output_ports={"main": None},
|
|
223
|
+
parameter_aliases={
|
|
224
|
+
"d_model": ("dim", "hidden_size"),
|
|
225
|
+
"num_heads": ("heads", "head"),
|
|
226
|
+
"backend": ("kernel",),
|
|
227
|
+
},
|
|
228
|
+
runtime_sources={
|
|
229
|
+
"d_model": "model_dim",
|
|
230
|
+
"num_heads": "heads",
|
|
231
|
+
"backend": "backend",
|
|
232
|
+
},
|
|
233
|
+
))
|
|
234
|
+
|
|
235
|
+
# Multi-input proof: the existing Studio Main/Skip lanes map directly to the
|
|
236
|
+
# original ResController API. Main carries the update tensor; Skip carries
|
|
237
|
+
# the residual stream. TensorGraph does not need a ResController branch.
|
|
238
|
+
API_COMPONENTS.register(APIComponentContract(
|
|
239
|
+
component_type="rescontroller",
|
|
240
|
+
import_key="rescontroller",
|
|
241
|
+
input_ports={"main": "update", "skip": "residual"},
|
|
242
|
+
output_ports={"main": None},
|
|
243
|
+
runtime_sources={
|
|
244
|
+
"backend": "backend",
|
|
245
|
+
},
|
|
246
|
+
))
|
|
247
|
+
# Stateful multi-input/multi-output proof: SAFFN exposes its original forward
|
|
248
|
+
# signature as named graph ports. The executor maps API return tuple[0] to
|
|
249
|
+
# ``main`` and tuple[1] to ``state``; no SAFFN branch is required in TensorGraph.
|
|
250
|
+
API_COMPONENTS.register(APIComponentContract(
|
|
251
|
+
component_type="saffn",
|
|
252
|
+
import_key="saffn",
|
|
253
|
+
input_ports={
|
|
254
|
+
"x": "x",
|
|
255
|
+
"esa_update": "esa_update",
|
|
256
|
+
"previous_esa": "previous_esa",
|
|
257
|
+
"previous_state": "previous_state",
|
|
258
|
+
},
|
|
259
|
+
output_ports={"main": 0, "state": 1},
|
|
260
|
+
parameter_aliases={
|
|
261
|
+
"d_model": ("dim", "hidden_size"),
|
|
262
|
+
"depth_embedding_dim": ("depth_dim",),
|
|
263
|
+
},
|
|
264
|
+
runtime_sources={
|
|
265
|
+
"d_model": "model_dim",
|
|
266
|
+
"backend": "backend",
|
|
267
|
+
},
|
|
268
|
+
input_initializers={
|
|
269
|
+
# At physical depth 0 there is no earlier signal/state. Studio supplies
|
|
270
|
+
# correctly shaped zeros only when these sockets are left unconnected.
|
|
271
|
+
"previous_esa": {"kind": "zeros_like", "source": "esa_update"},
|
|
272
|
+
"previous_state": {
|
|
273
|
+
# Use the original MLBricks helper so Studio does not duplicate
|
|
274
|
+
# StateAwareFFN's state-shape policy.
|
|
275
|
+
"kind": "module_method",
|
|
276
|
+
"source": "x",
|
|
277
|
+
"method": "initial_state",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
# VirtualStateAwareFFN preserves the exact StateAwareFFN forward contract and
|
|
283
|
+
# adds internal virtual refinements. It therefore reuses the same named graph
|
|
284
|
+
# wiring and first-depth initialization policy without a compiler branch.
|
|
285
|
+
API_COMPONENTS.register(APIComponentContract(
|
|
286
|
+
component_type="virtual_saffn",
|
|
287
|
+
import_key="virtual_saffn",
|
|
288
|
+
input_ports={
|
|
289
|
+
"x": "x",
|
|
290
|
+
"esa_update": "esa_update",
|
|
291
|
+
"previous_esa": "previous_esa",
|
|
292
|
+
"previous_state": "previous_state",
|
|
293
|
+
},
|
|
294
|
+
output_ports={"main": 0, "state": 1},
|
|
295
|
+
parameter_aliases={
|
|
296
|
+
"d_model": ("dim", "hidden_size"),
|
|
297
|
+
"depth_embedding_dim": ("depth_dim",),
|
|
298
|
+
},
|
|
299
|
+
runtime_sources={
|
|
300
|
+
"d_model": "model_dim",
|
|
301
|
+
"backend": "backend",
|
|
302
|
+
},
|
|
303
|
+
input_initializers={
|
|
304
|
+
"previous_esa": {"kind": "zeros_like", "source": "esa_update"},
|
|
305
|
+
"previous_state": {
|
|
306
|
+
"kind": "module_method",
|
|
307
|
+
"source": "x",
|
|
308
|
+
"method": "initial_state",
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
# MicroVirtualFFN is a standard one-input/one-output original MLBricks API.
|
|
314
|
+
# The default forward pass validates one configured refinement pass; callers
|
|
315
|
+
# can still use the full ``refine`` API through a custom API Function node.
|
|
316
|
+
API_COMPONENTS.register(APIComponentContract(
|
|
317
|
+
component_type="micro_ffn",
|
|
318
|
+
import_key="micro_ffn",
|
|
319
|
+
input_ports={"main": "x"},
|
|
320
|
+
output_ports={"main": None},
|
|
321
|
+
parameter_aliases={
|
|
322
|
+
"d_model": ("dim", "hidden_size"),
|
|
323
|
+
},
|
|
324
|
+
runtime_sources={
|
|
325
|
+
"d_model": "model_dim",
|
|
326
|
+
"backend": "backend",
|
|
327
|
+
},
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
# Vision classification path. Both current MLBricks Kit 1.0.0b2 families take
|
|
331
|
+
# raw image tensors [B,C,H,W] for Serpentine / ViT / CNN and return class logits.
|
|
332
|
+
API_COMPONENTS.register(VisionClassificationContract(
|
|
333
|
+
component_type="vesa",
|
|
334
|
+
import_key="vesa",
|
|
335
|
+
input_ports={"main": "images"},
|
|
336
|
+
output_ports={"main": None},
|
|
337
|
+
runtime_sources={"backend": "backend"},
|
|
338
|
+
))
|
|
339
|
+
|
|
340
|
+
API_COMPONENTS.register(VisionClassificationContract(
|
|
341
|
+
component_type="visualbolt",
|
|
342
|
+
import_key="visualbolt",
|
|
343
|
+
input_ports={"main": "images"},
|
|
344
|
+
output_ports={"main": None},
|
|
345
|
+
runtime_sources={"backend": "backend"},
|
|
346
|
+
))
|
|
347
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
|
|
9
|
+
from .import_pool import (
|
|
10
|
+
COMPONENT_IMPORTS,
|
|
11
|
+
CONFIG_KEYS,
|
|
12
|
+
IMPORT_POOL,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_SCHEMA_PATH = Path(__file__).with_name("mlbricks_api_schema.json")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# These components intentionally keep the richer source-derived Builder schema
|
|
19
|
+
# instead of replacing it with a plain constructor signature at runtime.
|
|
20
|
+
# SOUP accepts scalar-or-per-layer sequences/config mappings, while the primary
|
|
21
|
+
# ElasticBit 4-32 UI represents ElasticBit.RuntimeMatrix.from_auto.
|
|
22
|
+
SOURCE_DEFINED_FIELDS = {"soup", "elasticbit_runtime", "lm_head"}
|
|
23
|
+
|
|
24
|
+
CHOICES = {
|
|
25
|
+
"backend": ["auto", "native", "pytorch"],
|
|
26
|
+
"precision": ["fp32", "fp16", "bf16"],
|
|
27
|
+
"activation": ["gelu", "gelu_tanh", "relu", "silu", "swish", "tanh"],
|
|
28
|
+
"device": ["auto", "cpu", "cuda", "None"],
|
|
29
|
+
"engine": ["Serpentine", "ViT", "CNN", "Diffusion", "AR"],
|
|
30
|
+
"ffn": ["standard", "ffnbrick", "virtual_ffnbrick", "micro_ffnbrick"],
|
|
31
|
+
"residual": ["standard", "rescontroller"],
|
|
32
|
+
"norm": ["rmsnorm", "layernorm"],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
COMPONENT_CHOICES = {
|
|
36
|
+
"bolt": {
|
|
37
|
+
"position": ["none", "rope"],
|
|
38
|
+
},
|
|
39
|
+
"vesa": {
|
|
40
|
+
"position": ["auto", "none", "2d_sincos", "learned"],
|
|
41
|
+
"scan": ["cross", "horizontal", "vertical", "raster"],
|
|
42
|
+
},
|
|
43
|
+
"visualbolt": {
|
|
44
|
+
"position": ["auto", "none", "2d_sincos", "learned"],
|
|
45
|
+
"scan": ["cross", "horizontal", "vertical", "raster"],
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fallback_schema():
|
|
51
|
+
payload = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
52
|
+
return deepcopy(payload["components"])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _safe_default(value: Any):
|
|
56
|
+
if value is inspect._empty:
|
|
57
|
+
return None
|
|
58
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
59
|
+
return value
|
|
60
|
+
return str(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _field_type(name, annotation, default, choices=None):
|
|
64
|
+
choices = CHOICES if choices is None else choices
|
|
65
|
+
if name in choices:
|
|
66
|
+
return "select"
|
|
67
|
+
if isinstance(default, bool):
|
|
68
|
+
return "bool"
|
|
69
|
+
if isinstance(default, (int, float)):
|
|
70
|
+
return "number"
|
|
71
|
+
text = "" if annotation is inspect._empty else str(annotation).lower()
|
|
72
|
+
if "bool" in text:
|
|
73
|
+
return "bool"
|
|
74
|
+
if "int" in text or "float" in text:
|
|
75
|
+
return "number"
|
|
76
|
+
return "text"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _fields(obj, *, choices=None):
|
|
80
|
+
choices = CHOICES if choices is None else choices
|
|
81
|
+
sig = inspect.signature(obj)
|
|
82
|
+
out = []
|
|
83
|
+
for name, p in sig.parameters.items():
|
|
84
|
+
if name in {"self", "args", "kwargs"}:
|
|
85
|
+
continue
|
|
86
|
+
default = _safe_default(p.default)
|
|
87
|
+
item = {
|
|
88
|
+
"key": name,
|
|
89
|
+
"label": name.replace("_", " ").title(),
|
|
90
|
+
"type": _field_type(name, p.annotation, default, choices),
|
|
91
|
+
"required": p.default is inspect._empty,
|
|
92
|
+
"value": default,
|
|
93
|
+
"annotation": "" if p.annotation is inspect._empty else str(p.annotation),
|
|
94
|
+
}
|
|
95
|
+
if name in choices:
|
|
96
|
+
item["options"] = choices[name]
|
|
97
|
+
out.append(item)
|
|
98
|
+
return sig, out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _canonical_metadata(component_type: str, fallback: dict[str, Any]) -> dict[str, Any]:
|
|
102
|
+
info = IMPORT_POOL.import_info(component_type)
|
|
103
|
+
canonical_path = info.get("canonical_path")
|
|
104
|
+
public_name = info.get("canonical_symbol") or fallback.get("public_name")
|
|
105
|
+
payload = {
|
|
106
|
+
**fallback,
|
|
107
|
+
"available": True,
|
|
108
|
+
"runtime_available": None,
|
|
109
|
+
"source": "MLBricks source schema + lazy import pool",
|
|
110
|
+
"public_name": public_name,
|
|
111
|
+
"import_path": canonical_path or fallback.get("import_path"),
|
|
112
|
+
"import_module": info.get("canonical_module"),
|
|
113
|
+
"import_symbol": info.get("canonical_symbol"),
|
|
114
|
+
"import_candidates": [candidate.path for candidate in COMPONENT_IMPORTS.get(component_type, ())],
|
|
115
|
+
"loaded": bool(info.get("loaded")),
|
|
116
|
+
"resolved_from": info.get("resolved_from"),
|
|
117
|
+
"runtime_error": info.get("error"),
|
|
118
|
+
}
|
|
119
|
+
if info.get("config_path"):
|
|
120
|
+
config = deepcopy(payload.get("config_api") or {})
|
|
121
|
+
config.update({
|
|
122
|
+
"public_name": info.get("config_symbol"),
|
|
123
|
+
"import_path": info.get("config_path"),
|
|
124
|
+
"import_module": info.get("config_module"),
|
|
125
|
+
"import_symbol": info.get("config_symbol"),
|
|
126
|
+
})
|
|
127
|
+
payload["config_api"] = config
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _inspect_one(component_type: str, fallback: dict[str, Any]) -> dict[str, Any]:
|
|
132
|
+
result = _canonical_metadata(component_type, fallback)
|
|
133
|
+
try:
|
|
134
|
+
obj = IMPORT_POOL.resolve_component(component_type)
|
|
135
|
+
component_choices = {**CHOICES, **COMPONENT_CHOICES.get(component_type, {})}
|
|
136
|
+
sig, inspected_fields = _fields(obj, choices=component_choices)
|
|
137
|
+
source_defined = component_type in SOURCE_DEFINED_FIELDS
|
|
138
|
+
fields = deepcopy(fallback.get("parameters", [])) if source_defined else inspected_fields
|
|
139
|
+
|
|
140
|
+
config_info = result.get("config_api")
|
|
141
|
+
if component_type in CONFIG_KEYS:
|
|
142
|
+
cfg_obj = IMPORT_POOL.resolve_config(component_type)
|
|
143
|
+
cfg_choices = {**CHOICES, **COMPONENT_CHOICES.get(component_type, {})}
|
|
144
|
+
cfg_sig, cfg_fields = _fields(cfg_obj, choices=cfg_choices)
|
|
145
|
+
if len(cfg_fields) > 1:
|
|
146
|
+
fields = cfg_fields
|
|
147
|
+
config_info = {
|
|
148
|
+
**(config_info or {}),
|
|
149
|
+
"public_name": cfg_obj.__name__,
|
|
150
|
+
"signature": f"{cfg_obj.__name__}{cfg_sig}",
|
|
151
|
+
"parameters": cfg_fields,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
doc = inspect.getdoc(obj) or ""
|
|
155
|
+
runtime_available = True
|
|
156
|
+
runtime_error = None
|
|
157
|
+
if component_type == "elasticbit_runtime":
|
|
158
|
+
checker = getattr(obj, "native_runtime_available", None)
|
|
159
|
+
if callable(checker):
|
|
160
|
+
try:
|
|
161
|
+
runtime_available = bool(checker())
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
runtime_available = False
|
|
164
|
+
runtime_error = f"{type(exc).__name__}: {exc}"
|
|
165
|
+
if not runtime_available and runtime_error is None:
|
|
166
|
+
runtime_error = "ElasticBit runtime is not built in this environment."
|
|
167
|
+
|
|
168
|
+
info = IMPORT_POOL.import_info(component_type)
|
|
169
|
+
return {
|
|
170
|
+
**result,
|
|
171
|
+
"available": True,
|
|
172
|
+
"runtime_available": runtime_available,
|
|
173
|
+
"source": "runtime inspection + MLBricks source schema" if source_defined else "runtime inspection",
|
|
174
|
+
"public_name": fallback.get("public_name", obj.__name__) if source_defined else obj.__name__,
|
|
175
|
+
"signature": fallback.get("signature", f"{obj.__name__}{sig}") if source_defined else f"{obj.__name__}{sig}",
|
|
176
|
+
"description": fallback.get("description", "") if source_defined else (doc.splitlines()[0] if doc else fallback.get("description", "")),
|
|
177
|
+
"parameters": fields if fields else fallback.get("parameters", []),
|
|
178
|
+
"config_api": config_info,
|
|
179
|
+
"runtime_error": runtime_error,
|
|
180
|
+
"loaded": True,
|
|
181
|
+
"resolved_from": info.get("resolved_from"),
|
|
182
|
+
}
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
info = IMPORT_POOL.import_info(component_type)
|
|
185
|
+
return {
|
|
186
|
+
**result,
|
|
187
|
+
"available": True,
|
|
188
|
+
"runtime_available": False,
|
|
189
|
+
"loaded": False,
|
|
190
|
+
"source": "MLBricks source schema + lazy import pool",
|
|
191
|
+
"runtime_error": f"{type(exc).__name__}: {exc}",
|
|
192
|
+
"resolved_from": info.get("resolved_from"),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def discover_mlbricks_api(
|
|
197
|
+
component_types: Iterable[str] | None = None,
|
|
198
|
+
*,
|
|
199
|
+
eager: bool = False,
|
|
200
|
+
):
|
|
201
|
+
"""Return Builder API metadata without requiring eager MLBricks imports.
|
|
202
|
+
|
|
203
|
+
By default, the supplied source-derived schema is used and canonical import
|
|
204
|
+
routes are attached to each component. If ``eager=True`` (or a specific
|
|
205
|
+
``component_types`` iterable is supplied), only those requested components
|
|
206
|
+
are imported and inspected. This is the import-pool behavior used by the
|
|
207
|
+
Builder UI: adding a component can warm just that component's API instead of
|
|
208
|
+
importing the whole MLBricks package.
|
|
209
|
+
"""
|
|
210
|
+
result = _fallback_schema()
|
|
211
|
+
requested = set(str(x) for x in (component_types or ()))
|
|
212
|
+
inspect_all = bool(eager and component_types is None)
|
|
213
|
+
|
|
214
|
+
for component_type in COMPONENT_IMPORTS:
|
|
215
|
+
fallback = result.get(component_type, {})
|
|
216
|
+
if inspect_all or component_type in requested:
|
|
217
|
+
result[component_type] = _inspect_one(component_type, fallback)
|
|
218
|
+
else:
|
|
219
|
+
result[component_type] = _canonical_metadata(component_type, fallback)
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def refresh_component_api(component_type: str) -> dict[str, Any] | None:
|
|
225
|
+
component_type = str(component_type)
|
|
226
|
+
if component_type not in COMPONENT_IMPORTS:
|
|
227
|
+
return None
|
|
228
|
+
fallback = _fallback_schema().get(component_type, {})
|
|
229
|
+
return _inspect_one(component_type, fallback)
|