ivoryos 1.2.5__py3-none-any.whl → 1.4.4__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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +16 -246
- ivoryos/app.py +154 -0
- ivoryos/optimizer/ax_optimizer.py +55 -28
- ivoryos/optimizer/base_optimizer.py +20 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +3 -1
- ivoryos/routes/auth/auth.py +35 -8
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +58 -28
- ivoryos/routes/control/control_file.py +12 -15
- ivoryos/routes/control/control_new_device.py +21 -11
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +110 -44
- ivoryos/routes/data/templates/components/step_card.html +78 -13
- ivoryos/routes/data/templates/workflow_view.html +343 -113
- ivoryos/routes/design/design.py +59 -10
- ivoryos/routes/design/design_file.py +3 -3
- ivoryos/routes/design/design_step.py +43 -17
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +3 -0
- ivoryos/routes/execute/execute.py +82 -22
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/routes/library/library.py +9 -11
- ivoryos/routes/main/main.py +30 -2
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +259 -88
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +29 -11
- ivoryos/templates/base.html +61 -2
- ivoryos/utils/bo_campaign.py +18 -17
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +286 -60
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +52 -19
- ivoryos/utils/global_config.py +21 -0
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +80 -10
- ivoryos/utils/script_runner.py +573 -189
- ivoryos/utils/task_runner.py +69 -22
- ivoryos/utils/utils.py +48 -5
- ivoryos/version.py +1 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- ivoryos-1.4.4.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +133 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +80 -0
- tests/integration/test_route_control.py +94 -0
- tests/integration/test_route_database.py +61 -0
- tests/integration/test_route_design.py +36 -0
- tests/integration/test_route_main.py +35 -0
- tests/integration/test_sockets.py +26 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/api/api.py +0 -56
- ivoryos-1.2.5.dist-info/RECORD +0 -100
- ivoryos-1.2.5.dist-info/top_level.txt +0 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/licenses/LICENSE +0 -0
ivoryos/utils/db_models.py
CHANGED
|
@@ -27,6 +27,9 @@ class User(db.Model, UserMixin):
|
|
|
27
27
|
# email = db.Column(db.String)
|
|
28
28
|
hashPassword = db.Column(db.String(255))
|
|
29
29
|
|
|
30
|
+
# New columns for logo customization
|
|
31
|
+
settings = db.Column(JSONType, nullable=True)
|
|
32
|
+
|
|
30
33
|
# password = db.Column()
|
|
31
34
|
def __init__(self, username, password):
|
|
32
35
|
# self.id = id
|
|
@@ -122,7 +125,7 @@ class Script(db.Model):
|
|
|
122
125
|
pass
|
|
123
126
|
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
|
124
127
|
|
|
125
|
-
def update_by_uuid(self, uuid, args, output):
|
|
128
|
+
def update_by_uuid(self, uuid, args, output, batch_action=False):
|
|
126
129
|
action = self.find_by_uuid(uuid)
|
|
127
130
|
if not action:
|
|
128
131
|
return
|
|
@@ -134,6 +137,7 @@ class Script(db.Model):
|
|
|
134
137
|
pass
|
|
135
138
|
action['args'] = args
|
|
136
139
|
action['return'] = output
|
|
140
|
+
action['batch_action'] = batch_action
|
|
137
141
|
|
|
138
142
|
@staticmethod
|
|
139
143
|
def eval_list(args, arg_types):
|
|
@@ -268,7 +272,7 @@ class Script(db.Model):
|
|
|
268
272
|
if isinstance(value, str):
|
|
269
273
|
if value in output_variables:
|
|
270
274
|
var_type = output_variables[value]
|
|
271
|
-
kwargs[key] = {value
|
|
275
|
+
kwargs[key] = f"#{value}"
|
|
272
276
|
elif value.startswith("#"):
|
|
273
277
|
kwargs[key] = f"#{self.validate_function_name(value[1:])}"
|
|
274
278
|
else:
|
|
@@ -318,6 +322,12 @@ class Script(db.Model):
|
|
|
318
322
|
{"id": current_len + 2, "instrument": 'repeat', "action": 'endrepeat',
|
|
319
323
|
"args": {}, "return": '', "uuid": uid},
|
|
320
324
|
],
|
|
325
|
+
"pause":
|
|
326
|
+
[
|
|
327
|
+
{"id": current_len + 1, "instrument": 'pause', "action": "pause",
|
|
328
|
+
"args": {"statement": 1 if statement == '' else statement}, "return": '', "uuid": uid,
|
|
329
|
+
"arg_types": {"statement": "str"}}
|
|
330
|
+
],
|
|
321
331
|
}
|
|
322
332
|
action_list = logic_dict[logic_type]
|
|
323
333
|
self.currently_editing_script.extend(action_list)
|
|
@@ -364,28 +374,32 @@ class Script(db.Model):
|
|
|
364
374
|
:return: list of variable that require input
|
|
365
375
|
"""
|
|
366
376
|
configure = []
|
|
377
|
+
variables = self.get_variables()
|
|
378
|
+
# print(variables)
|
|
367
379
|
config_type_dict = {}
|
|
368
380
|
for action in self.script_dict[stype]:
|
|
369
381
|
args = action['args']
|
|
370
382
|
if args is not None:
|
|
371
383
|
if type(args) is not dict:
|
|
372
|
-
if type(args) is str and args.startswith("#")
|
|
373
|
-
|
|
374
|
-
|
|
384
|
+
if type(args) is str and args.startswith("#"):
|
|
385
|
+
key = args[1:]
|
|
386
|
+
if key not in (*variables, *configure):
|
|
387
|
+
configure.append(key)
|
|
388
|
+
config_type_dict[key] = action['arg_types']
|
|
375
389
|
|
|
376
390
|
else:
|
|
377
391
|
for arg in args:
|
|
378
|
-
if type(args[arg]) is str
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
392
|
+
if type(args[arg]) is str and args[arg].startswith("#"):
|
|
393
|
+
key = args[arg][1:]
|
|
394
|
+
if key not in (*variables, *configure):
|
|
395
|
+
configure.append(key)
|
|
396
|
+
if arg in action['arg_types']:
|
|
397
|
+
if action['arg_types'][arg] == '':
|
|
398
|
+
config_type_dict[key] = "any"
|
|
399
|
+
else:
|
|
400
|
+
config_type_dict[key] = action['arg_types'][arg]
|
|
385
401
|
else:
|
|
386
|
-
config_type_dict[
|
|
387
|
-
else:
|
|
388
|
-
config_type_dict[args[arg][1:]] = "any"
|
|
402
|
+
config_type_dict[key] = "any"
|
|
389
403
|
# todo
|
|
390
404
|
return configure, config_type_dict
|
|
391
405
|
|
|
@@ -396,7 +410,7 @@ class Script(db.Model):
|
|
|
396
410
|
"""
|
|
397
411
|
|
|
398
412
|
return_list = set([action['return'] for action in self.script_dict['script'] if not action['return'] == ''])
|
|
399
|
-
output_str = "
|
|
413
|
+
output_str = "{"
|
|
400
414
|
for i in return_list:
|
|
401
415
|
output_str += "'" + i + "':" + i + ","
|
|
402
416
|
output_str += "}"
|
|
@@ -428,21 +442,114 @@ class Script(db.Model):
|
|
|
428
442
|
:return: A dict containing script types as keys and lists of function body lines as values.
|
|
429
443
|
"""
|
|
430
444
|
line_collection = {}
|
|
445
|
+
|
|
431
446
|
for stype, func_str in exec_str_collection.items():
|
|
432
447
|
if func_str:
|
|
433
448
|
module = ast.parse(func_str)
|
|
434
|
-
func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
|
|
435
449
|
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
450
|
+
# Find the first function (regular or async)
|
|
451
|
+
func_def = next(
|
|
452
|
+
node for node in module.body
|
|
453
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Extract function body as source lines, skipping 'return' nodes
|
|
457
|
+
line_collection[stype] = [
|
|
458
|
+
ast_unparse(node) for node in func_def.body if not isinstance(node, ast.Return)
|
|
459
|
+
]
|
|
439
460
|
return line_collection
|
|
440
461
|
|
|
441
|
-
def
|
|
462
|
+
def render_script_lines(self, script_dict):
|
|
463
|
+
"""
|
|
464
|
+
Convert the script_dict structure into a dict of displayable Python-like lines,
|
|
465
|
+
keeping ID consistency for highlighting.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
def render_args(args):
|
|
469
|
+
if not args:
|
|
470
|
+
return ""
|
|
471
|
+
return ", ".join(f"{k}={v}" for k, v in args.items())
|
|
472
|
+
|
|
473
|
+
def parse_block(block):
|
|
474
|
+
lines = []
|
|
475
|
+
indent = 0
|
|
476
|
+
stack = []
|
|
477
|
+
|
|
478
|
+
for action in block:
|
|
479
|
+
act = action["action"]
|
|
480
|
+
_id = action["id"]
|
|
481
|
+
instrument = action["instrument"]
|
|
482
|
+
|
|
483
|
+
# Handle control structures
|
|
484
|
+
if act == "if":
|
|
485
|
+
stmt = action["args"].get("statement", "")
|
|
486
|
+
lines.append(" " * indent + f"if {stmt}:")
|
|
487
|
+
indent += 1
|
|
488
|
+
stack.append("if")
|
|
489
|
+
|
|
490
|
+
elif act == "else":
|
|
491
|
+
indent -= 1
|
|
492
|
+
lines.append(" " * indent + f"else:")
|
|
493
|
+
indent += 1
|
|
494
|
+
|
|
495
|
+
elif act in ("endif", "endwhile"):
|
|
496
|
+
if stack:
|
|
497
|
+
stack.pop()
|
|
498
|
+
indent = max(indent - 1, 0)
|
|
499
|
+
lines.append(" " * indent + f"# {act}")
|
|
500
|
+
|
|
501
|
+
elif act == "while":
|
|
502
|
+
stmt = action["args"].get("statement", "")
|
|
503
|
+
lines.append(" " * indent + f"while {stmt}:")
|
|
504
|
+
indent += 1
|
|
505
|
+
stack.append("while")
|
|
506
|
+
|
|
507
|
+
elif act == "wait":
|
|
508
|
+
stmt = action["args"].get("statement", "")
|
|
509
|
+
lines.append(" " * indent + f"time.sleep({stmt})")
|
|
510
|
+
|
|
511
|
+
elif instrument == "variable":
|
|
512
|
+
stmt = action["args"].get("statement", "")
|
|
513
|
+
lines.append(" " * indent + f"{act} = {stmt}")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
else:
|
|
517
|
+
# Regular function call
|
|
518
|
+
instr = action["instrument"]
|
|
519
|
+
args = render_args(action.get("args", {}))
|
|
520
|
+
ret = action.get("return")
|
|
521
|
+
line = " " * indent
|
|
522
|
+
if ret:
|
|
523
|
+
line += f"{ret} = {instr}.{act}({args})"
|
|
524
|
+
else:
|
|
525
|
+
line += f"{instr}.{act}({args})"
|
|
526
|
+
lines.append(line)
|
|
527
|
+
|
|
528
|
+
# Ensure empty control blocks get "pass"
|
|
529
|
+
final_lines = []
|
|
530
|
+
for i, line in enumerate(lines):
|
|
531
|
+
final_lines.append(line)
|
|
532
|
+
# if line.strip().startswith("else") and (
|
|
533
|
+
# i == len(lines) - 1 or lines[i + 1].startswith("#") or "endif" in lines[i + 1]
|
|
534
|
+
# ):
|
|
535
|
+
# final_lines.append(" " * (indent) + "pass")
|
|
536
|
+
|
|
537
|
+
return final_lines
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
"prep": parse_block(script_dict.get("prep", [])),
|
|
541
|
+
"script": parse_block(script_dict.get("script", [])),
|
|
542
|
+
"cleanup": parse_block(script_dict.get("cleanup", []))
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
def compile(self, script_path=None, batch=False, mode="sample"):
|
|
442
546
|
"""
|
|
443
547
|
Compile the current script to a Python file.
|
|
444
548
|
:return: String to write to a Python file.
|
|
445
549
|
"""
|
|
550
|
+
self.needs_call_human = False
|
|
551
|
+
self.blocks_included = False
|
|
552
|
+
|
|
446
553
|
self.sort_actions()
|
|
447
554
|
run_name = self.name if self.name else "untitled"
|
|
448
555
|
run_name = self.validate_function_name(run_name)
|
|
@@ -450,7 +557,8 @@ class Script(db.Model):
|
|
|
450
557
|
|
|
451
558
|
for i in self.stypes:
|
|
452
559
|
if self.script_dict[i]:
|
|
453
|
-
|
|
560
|
+
is_async = any(a.get("coroutine", False) for a in self.script_dict[i])
|
|
561
|
+
func_str = self._generate_function_header(run_name, i, is_async, batch) + self._generate_function_body(i, batch, mode)
|
|
454
562
|
exec_str_collection[i] = func_str
|
|
455
563
|
if script_path:
|
|
456
564
|
self._write_to_file(script_path, run_name, exec_str_collection)
|
|
@@ -468,50 +576,70 @@ class Script(db.Model):
|
|
|
468
576
|
name += '_'
|
|
469
577
|
return name
|
|
470
578
|
|
|
471
|
-
def _generate_function_header(self, run_name, stype):
|
|
579
|
+
def _generate_function_header(self, run_name, stype, is_async, batch=False):
|
|
472
580
|
"""
|
|
473
581
|
Generate the function header.
|
|
474
582
|
"""
|
|
475
583
|
configure, config_type = self.config(stype)
|
|
476
|
-
|
|
477
584
|
configure = [param + f":{param_type}" if not param_type == "any" else param for param, param_type in
|
|
478
585
|
config_type.items()]
|
|
479
586
|
|
|
480
587
|
script_type = f"_{stype}" if stype != "script" else ""
|
|
481
|
-
|
|
588
|
+
async_str = "async " if is_async else ""
|
|
589
|
+
function_header = f"{async_str}def {run_name}{script_type}("
|
|
482
590
|
|
|
483
591
|
if stype == "script":
|
|
484
|
-
|
|
485
|
-
|
|
592
|
+
if batch:
|
|
593
|
+
function_header += "param_list" if configure else "n: int"
|
|
594
|
+
else:
|
|
595
|
+
function_header += ", ".join(configure)
|
|
486
596
|
function_header += "):"
|
|
487
|
-
|
|
597
|
+
|
|
598
|
+
if stype == "script" and batch:
|
|
599
|
+
function_header += self.indent(1) + f'"""Batch mode is experimental and may have bugs."""'
|
|
488
600
|
return function_header
|
|
489
601
|
|
|
490
|
-
def _generate_function_body(self, stype):
|
|
602
|
+
def _generate_function_body(self, stype, batch=False, mode="sample"):
|
|
491
603
|
"""
|
|
492
604
|
Generate the function body for each type in stypes.
|
|
493
605
|
"""
|
|
494
606
|
body = ''
|
|
495
607
|
indent_unit = 1
|
|
608
|
+
if batch and stype == "script":
|
|
609
|
+
return_str, return_list = self.config_return()
|
|
610
|
+
configure, config_type = self.config(stype)
|
|
611
|
+
if not configure:
|
|
612
|
+
body += self.indent(indent_unit) + "param_list = [{} for _ in range(n)]"
|
|
613
|
+
for index, action in enumerate(self.script_dict[stype]):
|
|
614
|
+
text, indent_unit = self._process_action(indent_unit, action, index, stype, batch)
|
|
615
|
+
body += text
|
|
616
|
+
if return_list:
|
|
617
|
+
# body += self.indent(indent_unit) + f"result_list.append({return_str})"
|
|
618
|
+
body += self.indent(indent_unit) + "return param_list"
|
|
496
619
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
620
|
+
else:
|
|
621
|
+
for index, action in enumerate(self.script_dict[stype]):
|
|
622
|
+
text, indent_unit = self._process_action(indent_unit, action, index, stype)
|
|
623
|
+
body += text
|
|
624
|
+
return_str, return_list = self.config_return()
|
|
625
|
+
if return_list and stype == "script":
|
|
626
|
+
body += self.indent(indent_unit) + f"return {return_str}"
|
|
503
627
|
return body
|
|
504
628
|
|
|
505
|
-
def _process_action(self, indent_unit, action, index, stype):
|
|
629
|
+
def _process_action(self, indent_unit, action, index, stype, batch=False, mode="sample"):
|
|
506
630
|
"""
|
|
507
631
|
Process each action within the script dictionary.
|
|
508
632
|
"""
|
|
633
|
+
configure, config_type = self.config(stype)
|
|
634
|
+
|
|
509
635
|
instrument = action['instrument']
|
|
510
636
|
statement = action['args'].get('statement')
|
|
511
637
|
args = self._process_args(action['args'])
|
|
512
638
|
|
|
513
639
|
save_data = action['return']
|
|
514
640
|
action_name = action['action']
|
|
641
|
+
batch_action = action.get("batch_action", False)
|
|
642
|
+
|
|
515
643
|
next_action = self._get_next_action(stype, index)
|
|
516
644
|
# print(args)
|
|
517
645
|
if instrument == 'if':
|
|
@@ -519,16 +647,29 @@ class Script(db.Model):
|
|
|
519
647
|
elif instrument == 'while':
|
|
520
648
|
return self._process_while(indent_unit, action_name, statement, next_action)
|
|
521
649
|
elif instrument == 'variable':
|
|
650
|
+
if batch:
|
|
651
|
+
return self.indent(indent_unit) + "for param in param_list:" + self.indent(indent_unit + 1) + f"param['{action_name}'] = {statement}", indent_unit
|
|
652
|
+
|
|
522
653
|
return self.indent(indent_unit) + f"{action_name} = {statement}", indent_unit
|
|
523
654
|
elif instrument == 'wait':
|
|
655
|
+
if batch:
|
|
656
|
+
return f"{self.indent(indent_unit)}for param in param_list:" + f"{self.indent(indent_unit+1)}time.sleep({statement})", indent_unit
|
|
524
657
|
return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
|
|
525
658
|
elif instrument == 'repeat':
|
|
659
|
+
|
|
526
660
|
return self._process_repeat(indent_unit, action_name, statement, next_action)
|
|
527
|
-
|
|
661
|
+
elif instrument == 'pause':
|
|
662
|
+
self.needs_call_human = True
|
|
663
|
+
if batch:
|
|
664
|
+
return f"{self.indent(indent_unit)}for param in param_list:" + f"{self.indent(indent_unit+1)}pause('{statement}')", indent_unit
|
|
665
|
+
return f"{self.indent(indent_unit)}pause('{statement}')", indent_unit
|
|
666
|
+
# todo
|
|
528
667
|
# elif instrument == 'registered_workflows':
|
|
529
668
|
# return inspect.getsource(my_function)
|
|
530
669
|
else:
|
|
531
|
-
|
|
670
|
+
is_async = action.get("coroutine", False)
|
|
671
|
+
dynamic_arg = len(self.get_variables()) > 0
|
|
672
|
+
return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data, is_async, dynamic_arg, batch, batch_action)
|
|
532
673
|
|
|
533
674
|
def _process_args(self, args):
|
|
534
675
|
"""
|
|
@@ -588,23 +729,44 @@ class Script(db.Model):
|
|
|
588
729
|
indent_unit -= 1
|
|
589
730
|
return exec_string, indent_unit
|
|
590
731
|
|
|
591
|
-
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data
|
|
732
|
+
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data, is_async=False, dynamic_arg=False,
|
|
733
|
+
batch=False, batch_action=False):
|
|
592
734
|
"""
|
|
593
735
|
Process actions related to instruments.
|
|
594
736
|
"""
|
|
737
|
+
async_str = "await " if is_async else ""
|
|
738
|
+
|
|
739
|
+
function_call = f"{instrument}.{action}"
|
|
740
|
+
if instrument.startswith("blocks"):
|
|
741
|
+
self.blocks_included = True
|
|
742
|
+
function_call = action
|
|
595
743
|
|
|
596
|
-
if isinstance(args, dict):
|
|
744
|
+
if isinstance(args, dict) and args != {}:
|
|
597
745
|
args_str = self._process_dict_args(args)
|
|
598
|
-
single_line = f"{
|
|
746
|
+
single_line = f"{async_str}{function_call}(**{args_str})"
|
|
599
747
|
elif isinstance(args, str):
|
|
600
|
-
single_line = f"{
|
|
748
|
+
single_line = f"{function_call} = {args}"
|
|
601
749
|
else:
|
|
602
|
-
single_line = f"{
|
|
750
|
+
single_line = f"{async_str}{function_call}()"
|
|
603
751
|
|
|
604
|
-
if save_data:
|
|
605
|
-
save_data += " = "
|
|
606
752
|
|
|
607
|
-
|
|
753
|
+
save_data_str = save_data + " = " if save_data else ''
|
|
754
|
+
|
|
755
|
+
if batch and not batch_action:
|
|
756
|
+
arg_list = [args[arg][1:] for arg in args if isinstance(args[arg], str) and args[arg].startswith("#")]
|
|
757
|
+
param_str = [f"param['{arg_list}']" for arg_list in arg_list if arg_list]
|
|
758
|
+
args_str = self.indent(indent_unit + 1) + ", ".join(arg_list) + " = " + ", ".join(param_str) if arg_list else ""
|
|
759
|
+
if dynamic_arg:
|
|
760
|
+
for_string = self.indent(indent_unit) + "for param in param_list:" + args_str
|
|
761
|
+
else:
|
|
762
|
+
for_string = self.indent(indent_unit) + "for i in range(n):"
|
|
763
|
+
output_code = for_string + self.indent(indent_unit + 1) + save_data_str + single_line
|
|
764
|
+
if save_data:
|
|
765
|
+
output_code = output_code + self.indent(indent_unit + 1) + f"param['{save_data}'] = {save_data}"
|
|
766
|
+
else:
|
|
767
|
+
output_code = self.indent(indent_unit) + save_data_str + single_line
|
|
768
|
+
|
|
769
|
+
return output_code, indent_unit
|
|
608
770
|
|
|
609
771
|
def _process_dict_args(self, args):
|
|
610
772
|
"""
|
|
@@ -616,12 +778,21 @@ class Script(db.Model):
|
|
|
616
778
|
args_str = args_str.replace(f"'#{args[arg][1:]}'", args[arg][1:])
|
|
617
779
|
elif isinstance(args[arg], dict):
|
|
618
780
|
# print(args[arg])
|
|
619
|
-
|
|
781
|
+
if not args[arg]:
|
|
782
|
+
continue
|
|
783
|
+
# Extract the variable name (first key in the dict)
|
|
620
784
|
value = next(iter(args[arg]))
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
785
|
+
var_type = args[arg].get(value)
|
|
786
|
+
|
|
787
|
+
# Only process if it's a function_output variable reference
|
|
788
|
+
if var_type == "function_output":
|
|
789
|
+
variables = self.get_variables()
|
|
790
|
+
if value not in variables:
|
|
791
|
+
raise ValueError(f"Variable ({value}) is not defined.")
|
|
792
|
+
# Replace the dict string representation with just the variable name
|
|
793
|
+
args_str = args_str.replace(f"{args[arg]}", value)
|
|
794
|
+
|
|
795
|
+
# elif self._is_variable(arg):
|
|
625
796
|
# print("is variable")
|
|
626
797
|
# args_str = args_str.replace(f"'{args[arg]}'", args[arg])
|
|
627
798
|
return args_str
|
|
@@ -640,7 +811,7 @@ class Script(db.Model):
|
|
|
640
811
|
"""
|
|
641
812
|
return arg in self.script_dict and self.script_dict[arg].get("arg_types") == "variable"
|
|
642
813
|
|
|
643
|
-
def _write_to_file(self, script_path, run_name, exec_string):
|
|
814
|
+
def _write_to_file(self, script_path, run_name, exec_string, call_human=False):
|
|
644
815
|
"""
|
|
645
816
|
Write the compiled script to a file.
|
|
646
817
|
"""
|
|
@@ -650,10 +821,30 @@ class Script(db.Model):
|
|
|
650
821
|
else:
|
|
651
822
|
s.write("deck = None")
|
|
652
823
|
s.write("\nimport time")
|
|
824
|
+
if self.blocks_included:
|
|
825
|
+
s.write(f"\n{self._create_block_import()}")
|
|
826
|
+
if self.needs_call_human:
|
|
827
|
+
s.write("""\n\ndef pause(reason="Manual intervention required"):\n\tprint(f"\\nHUMAN INTERVENTION REQUIRED: {reason}")\n\tinput("Press Enter to continue...\\n")""")
|
|
828
|
+
|
|
653
829
|
for i in exec_string.values():
|
|
654
830
|
s.write(f"\n\n\n{i}")
|
|
655
831
|
|
|
832
|
+
def _create_block_import(self):
|
|
833
|
+
imports = {}
|
|
834
|
+
from ivoryos.utils.decorators import BUILDING_BLOCKS
|
|
835
|
+
for category, methods in BUILDING_BLOCKS.items():
|
|
836
|
+
for method_name, meta in methods.items():
|
|
837
|
+
func = meta["func"]
|
|
838
|
+
module = meta["path"]
|
|
839
|
+
name = func.__name__
|
|
840
|
+
imports.setdefault(module, set()).add(name)
|
|
841
|
+
lines = []
|
|
842
|
+
for module, funcs in imports.items():
|
|
843
|
+
lines.append(f"from {module} import {', '.join(sorted(funcs))}")
|
|
844
|
+
return "\n".join(lines)
|
|
845
|
+
|
|
656
846
|
class WorkflowRun(db.Model):
|
|
847
|
+
"""Represents the entire experiment"""
|
|
657
848
|
__tablename__ = 'workflow_runs'
|
|
658
849
|
|
|
659
850
|
id = db.Column(db.Integer, primary_key=True)
|
|
@@ -662,30 +853,65 @@ class WorkflowRun(db.Model):
|
|
|
662
853
|
start_time = db.Column(db.DateTime, default=datetime.now())
|
|
663
854
|
end_time = db.Column(db.DateTime)
|
|
664
855
|
data_path = db.Column(db.String(256))
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
856
|
+
repeat_mode = db.Column(db.String(64), default="none") # static_repeat, sweep, optimizer
|
|
857
|
+
|
|
858
|
+
# A run contains multiple iterations
|
|
859
|
+
phases = db.relationship(
|
|
860
|
+
'WorkflowPhase',
|
|
861
|
+
backref='workflow_runs', # Clearer back-reference name
|
|
668
862
|
cascade='all, delete-orphan',
|
|
669
|
-
|
|
863
|
+
lazy='dynamic' # Good for handling many iterations
|
|
670
864
|
)
|
|
671
865
|
def as_dict(self):
|
|
672
866
|
dict = self.__dict__
|
|
673
867
|
dict.pop('_sa_instance_state', None)
|
|
674
868
|
return dict
|
|
675
869
|
|
|
870
|
+
class WorkflowPhase(db.Model):
|
|
871
|
+
"""Represents a single function call within a WorkflowRun."""
|
|
872
|
+
__tablename__ = 'workflow_phases'
|
|
873
|
+
|
|
874
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
875
|
+
# Foreign key to link this iteration to its parent run
|
|
876
|
+
run_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
|
|
877
|
+
|
|
878
|
+
# NEW: Store iteration-specific parameters here
|
|
879
|
+
name = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
|
880
|
+
repeat_index = db.Column(db.Integer, default=0)
|
|
881
|
+
|
|
882
|
+
parameters = db.Column(JSONType) # Use db.JSON for general support
|
|
883
|
+
outputs = db.Column(JSONType)
|
|
884
|
+
start_time = db.Column(db.DateTime, default=datetime.now)
|
|
885
|
+
end_time = db.Column(db.DateTime)
|
|
886
|
+
|
|
887
|
+
# An iteration contains multiple steps
|
|
888
|
+
steps = db.relationship(
|
|
889
|
+
'WorkflowStep',
|
|
890
|
+
backref='workflow_phases', # Clearer back-reference name
|
|
891
|
+
cascade='all, delete-orphan'
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
def as_dict(self):
|
|
895
|
+
dict = self.__dict__.copy()
|
|
896
|
+
dict.pop('_sa_instance_state', None)
|
|
897
|
+
return dict
|
|
898
|
+
|
|
676
899
|
class WorkflowStep(db.Model):
|
|
677
900
|
__tablename__ = 'workflow_steps'
|
|
678
901
|
|
|
679
902
|
id = db.Column(db.Integer, primary_key=True)
|
|
680
|
-
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=
|
|
903
|
+
# workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=True)
|
|
904
|
+
phase_id = db.Column(db.Integer, db.ForeignKey('workflow_phases.id', ondelete='CASCADE'), nullable=True)
|
|
681
905
|
|
|
682
|
-
phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
|
683
|
-
repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
|
|
906
|
+
# phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
|
907
|
+
# repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
|
|
684
908
|
step_index = db.Column(db.Integer, default=0)
|
|
685
909
|
method_name = db.Column(db.String(128), nullable=False)
|
|
686
910
|
start_time = db.Column(db.DateTime)
|
|
687
911
|
end_time = db.Column(db.DateTime)
|
|
688
912
|
run_error = db.Column(db.Boolean, default=False)
|
|
913
|
+
output = db.Column(JSONType, default={})
|
|
914
|
+
# Using as_dict method from ModelBase
|
|
689
915
|
|
|
690
916
|
def as_dict(self):
|
|
691
917
|
dict = self.__dict__.copy()
|
|
@@ -702,7 +928,7 @@ class SingleStep(db.Model):
|
|
|
702
928
|
start_time = db.Column(db.DateTime)
|
|
703
929
|
end_time = db.Column(db.DateTime)
|
|
704
930
|
run_error = db.Column(db.String(128))
|
|
705
|
-
output = db.Column(JSONType)
|
|
931
|
+
output = db.Column(JSONType, nullable=True)
|
|
706
932
|
|
|
707
933
|
def as_dict(self):
|
|
708
934
|
dict = self.__dict__.copy()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
BUILDING_BLOCKS = {}
|
|
5
|
+
|
|
6
|
+
def block(_func=None, *, category="general"):
|
|
7
|
+
def decorator(func):
|
|
8
|
+
if category not in BUILDING_BLOCKS:
|
|
9
|
+
BUILDING_BLOCKS[category] = {}
|
|
10
|
+
if func.__module__ == "__main__":
|
|
11
|
+
file_path = inspect.getfile(func) # e.g. /path/to/math_blocks.py
|
|
12
|
+
module = os.path.splitext(os.path.basename(file_path))[0]
|
|
13
|
+
else:
|
|
14
|
+
module = func.__module__
|
|
15
|
+
BUILDING_BLOCKS[category][func.__name__] = {
|
|
16
|
+
"func": func,
|
|
17
|
+
"signature": inspect.signature(func),
|
|
18
|
+
"docstring": inspect.getdoc(func),
|
|
19
|
+
"coroutine": inspect.iscoroutinefunction(func),
|
|
20
|
+
"path": module
|
|
21
|
+
}
|
|
22
|
+
return func
|
|
23
|
+
if _func is None:
|
|
24
|
+
return decorator
|
|
25
|
+
else:
|
|
26
|
+
return decorator(_func)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BlockNamespace:
|
|
30
|
+
"""[not in use] Expose methods for one block category as attributes."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, methods):
|
|
33
|
+
for name, meta in methods.items():
|
|
34
|
+
setattr(self, name, meta["func"])
|