ivoryos 1.0.9__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.
Files changed (107) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +17 -207
  3. ivoryos/app.py +154 -0
  4. ivoryos/config.py +1 -0
  5. ivoryos/optimizer/ax_optimizer.py +191 -0
  6. ivoryos/optimizer/base_optimizer.py +84 -0
  7. ivoryos/optimizer/baybe_optimizer.py +193 -0
  8. ivoryos/optimizer/nimo_optimizer.py +173 -0
  9. ivoryos/optimizer/registry.py +11 -0
  10. ivoryos/routes/auth/auth.py +43 -14
  11. ivoryos/routes/auth/templates/change_password.html +32 -0
  12. ivoryos/routes/control/control.py +101 -366
  13. ivoryos/routes/control/control_file.py +33 -0
  14. ivoryos/routes/control/control_new_device.py +152 -0
  15. ivoryos/routes/control/templates/controllers.html +193 -0
  16. ivoryos/routes/control/templates/controllers_new.html +112 -0
  17. ivoryos/routes/control/utils.py +40 -0
  18. ivoryos/routes/data/data.py +197 -0
  19. ivoryos/routes/data/templates/components/step_card.html +78 -0
  20. ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
  21. ivoryos/routes/data/templates/workflow_view.html +360 -0
  22. ivoryos/routes/design/__init__.py +4 -0
  23. ivoryos/routes/design/design.py +348 -657
  24. ivoryos/routes/design/design_file.py +68 -0
  25. ivoryos/routes/design/design_step.py +171 -0
  26. ivoryos/routes/design/templates/components/action_form.html +53 -0
  27. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  28. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  29. ivoryos/routes/design/templates/components/canvas.html +5 -0
  30. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  31. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  32. ivoryos/routes/design/templates/components/canvas_main.html +39 -0
  33. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  34. ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
  35. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  36. ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
  37. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  38. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  39. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  40. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  41. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  42. ivoryos/routes/design/templates/components/modals.html +6 -0
  43. ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  44. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  45. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  46. ivoryos/routes/design/templates/experiment_builder.html +44 -0
  47. ivoryos/routes/execute/__init__.py +0 -0
  48. ivoryos/routes/execute/execute.py +377 -0
  49. ivoryos/routes/execute/execute_file.py +78 -0
  50. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  51. ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  52. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  53. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  54. ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  55. ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  56. ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  57. ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
  58. ivoryos/routes/execute/templates/experiment_run.html +30 -0
  59. ivoryos/routes/library/__init__.py +0 -0
  60. ivoryos/routes/library/library.py +157 -0
  61. ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
  62. ivoryos/routes/main/main.py +31 -3
  63. ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
  64. ivoryos/server.py +180 -0
  65. ivoryos/socket_handlers.py +52 -0
  66. ivoryos/static/ivoryos_logo.png +0 -0
  67. ivoryos/static/js/action_handlers.js +384 -0
  68. ivoryos/static/js/db_delete.js +23 -0
  69. ivoryos/static/js/script_metadata.js +39 -0
  70. ivoryos/static/js/socket_handler.js +40 -5
  71. ivoryos/static/js/sortable_design.js +107 -56
  72. ivoryos/static/js/ui_state.js +114 -0
  73. ivoryos/templates/base.html +67 -8
  74. ivoryos/utils/bo_campaign.py +180 -3
  75. ivoryos/utils/client_proxy.py +267 -36
  76. ivoryos/utils/db_models.py +300 -65
  77. ivoryos/utils/decorators.py +34 -0
  78. ivoryos/utils/form.py +63 -29
  79. ivoryos/utils/global_config.py +34 -1
  80. ivoryos/utils/nest_script.py +314 -0
  81. ivoryos/utils/py_to_json.py +295 -0
  82. ivoryos/utils/script_runner.py +599 -165
  83. ivoryos/utils/serilize.py +201 -0
  84. ivoryos/utils/task_runner.py +71 -21
  85. ivoryos/utils/utils.py +50 -6
  86. ivoryos/version.py +1 -1
  87. ivoryos-1.4.4.dist-info/METADATA +263 -0
  88. ivoryos-1.4.4.dist-info/RECORD +119 -0
  89. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
  90. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
  91. tests/unit/test_type_conversion.py +42 -0
  92. tests/unit/test_util.py +3 -0
  93. ivoryos/routes/control/templates/control/controllers.html +0 -78
  94. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  95. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  96. ivoryos/routes/database/database.py +0 -306
  97. ivoryos/routes/database/templates/database/step_card.html +0 -7
  98. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  99. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  100. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  101. ivoryos-1.0.9.dist-info/METADATA +0 -218
  102. ivoryos-1.0.9.dist-info/RECORD +0 -61
  103. /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
  104. /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
  105. /ivoryos/routes/{database → data}/__init__.py +0 -0
  106. /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
  107. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
@@ -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):
@@ -265,11 +269,20 @@ class Script(db.Model):
265
269
  output_variables: Dict[str, str] = self.get_variables()
266
270
  # print(output_variables)
267
271
  for key, value in kwargs.items():
268
- if type(value) is str and value in output_variables:
269
- var_type = output_variables[value]
270
- kwargs[key] = {value: var_type}
271
- if isinstance(value, str) and value.startswith("#"):
272
- kwargs[key] = f"#{self.validate_function_name(value[1:])}"
272
+ if isinstance(value, str):
273
+ if value in output_variables:
274
+ var_type = output_variables[value]
275
+ kwargs[key] = f"#{value}"
276
+ elif value.startswith("#"):
277
+ kwargs[key] = f"#{self.validate_function_name(value[1:])}"
278
+ else:
279
+ # attempt to convert to numerical or bool value for args with no type hint
280
+ try:
281
+ converted = ast.literal_eval(value)
282
+ if isinstance(converted, (int, float, bool)):
283
+ kwargs[key] = converted
284
+ except (ValueError, SyntaxError):
285
+ pass
273
286
  return kwargs
274
287
 
275
288
  def add_logic_action(self, logic_type: str, statement, insert_position=None):
@@ -309,6 +322,12 @@ class Script(db.Model):
309
322
  {"id": current_len + 2, "instrument": 'repeat', "action": 'endrepeat',
310
323
  "args": {}, "return": '', "uuid": uid},
311
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
+ ],
312
331
  }
313
332
  action_list = logic_dict[logic_type]
314
333
  self.currently_editing_script.extend(action_list)
@@ -355,28 +374,32 @@ class Script(db.Model):
355
374
  :return: list of variable that require input
356
375
  """
357
376
  configure = []
377
+ variables = self.get_variables()
378
+ # print(variables)
358
379
  config_type_dict = {}
359
380
  for action in self.script_dict[stype]:
360
381
  args = action['args']
361
382
  if args is not None:
362
383
  if type(args) is not dict:
363
- if type(args) is str and args.startswith("#") and not args[1:] in configure:
364
- configure.append(args[1:])
365
- config_type_dict[args[1:]] = action['arg_types']
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']
366
389
 
367
390
  else:
368
391
  for arg in args:
369
- if type(args[arg]) is str \
370
- and args[arg].startswith("#") \
371
- and not args[arg][1:] in configure:
372
- configure.append(args[arg][1:])
373
- if arg in action['arg_types']:
374
- if action['arg_types'][arg] == '':
375
- config_type_dict[args[arg][1:]] = "any"
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]
376
401
  else:
377
- config_type_dict[args[arg][1:]] = action['arg_types'][arg]
378
- else:
379
- config_type_dict[args[arg][1:]] = "any"
402
+ config_type_dict[key] = "any"
380
403
  # todo
381
404
  return configure, config_type_dict
382
405
 
@@ -387,7 +410,7 @@ class Script(db.Model):
387
410
  """
388
411
 
389
412
  return_list = set([action['return'] for action in self.script_dict['script'] if not action['return'] == ''])
390
- output_str = "return {"
413
+ output_str = "{"
391
414
  for i in return_list:
392
415
  output_str += "'" + i + "':" + i + ","
393
416
  output_str += "}"
@@ -419,21 +442,114 @@ class Script(db.Model):
419
442
  :return: A dict containing script types as keys and lists of function body lines as values.
420
443
  """
421
444
  line_collection = {}
445
+
422
446
  for stype, func_str in exec_str_collection.items():
423
447
  if func_str:
424
448
  module = ast.parse(func_str)
425
- func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
426
449
 
427
- # Extract function body as source lines
428
- line_collection[stype] = [ast_unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
429
- # print(line_collection[stype])
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
+ ]
430
460
  return line_collection
431
461
 
432
- def compile(self, script_path=None):
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"):
433
546
  """
434
547
  Compile the current script to a Python file.
435
548
  :return: String to write to a Python file.
436
549
  """
550
+ self.needs_call_human = False
551
+ self.blocks_included = False
552
+
437
553
  self.sort_actions()
438
554
  run_name = self.name if self.name else "untitled"
439
555
  run_name = self.validate_function_name(run_name)
@@ -441,7 +557,8 @@ class Script(db.Model):
441
557
 
442
558
  for i in self.stypes:
443
559
  if self.script_dict[i]:
444
- func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
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)
445
562
  exec_str_collection[i] = func_str
446
563
  if script_path:
447
564
  self._write_to_file(script_path, run_name, exec_str_collection)
@@ -459,50 +576,70 @@ class Script(db.Model):
459
576
  name += '_'
460
577
  return name
461
578
 
462
- def _generate_function_header(self, run_name, stype):
579
+ def _generate_function_header(self, run_name, stype, is_async, batch=False):
463
580
  """
464
581
  Generate the function header.
465
582
  """
466
583
  configure, config_type = self.config(stype)
467
-
468
- configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
584
+ configure = [param + f":{param_type}" if not param_type == "any" else param for param, param_type in
469
585
  config_type.items()]
470
586
 
471
587
  script_type = f"_{stype}" if stype != "script" else ""
472
- function_header = f"def {run_name}{script_type}("
588
+ async_str = "async " if is_async else ""
589
+ function_header = f"{async_str}def {run_name}{script_type}("
473
590
 
474
591
  if stype == "script":
475
- function_header += ", ".join(configure)
476
-
592
+ if batch:
593
+ function_header += "param_list" if configure else "n: int"
594
+ else:
595
+ function_header += ", ".join(configure)
477
596
  function_header += "):"
478
- # function_header += self.indent(1) + f"global {run_name}_{stype}"
597
+
598
+ if stype == "script" and batch:
599
+ function_header += self.indent(1) + f'"""Batch mode is experimental and may have bugs."""'
479
600
  return function_header
480
601
 
481
- def _generate_function_body(self, stype):
602
+ def _generate_function_body(self, stype, batch=False, mode="sample"):
482
603
  """
483
604
  Generate the function body for each type in stypes.
484
605
  """
485
606
  body = ''
486
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"
487
619
 
488
- for index, action in enumerate(self.script_dict[stype]):
489
- text, indent_unit = self._process_action(indent_unit, action, index, stype)
490
- body += text
491
- return_str, return_list = self.config_return()
492
- if return_list and stype == "script":
493
- body += self.indent(indent_unit) + return_str
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}"
494
627
  return body
495
628
 
496
- def _process_action(self, indent_unit, action, index, stype):
629
+ def _process_action(self, indent_unit, action, index, stype, batch=False, mode="sample"):
497
630
  """
498
631
  Process each action within the script dictionary.
499
632
  """
633
+ configure, config_type = self.config(stype)
634
+
500
635
  instrument = action['instrument']
501
636
  statement = action['args'].get('statement')
502
637
  args = self._process_args(action['args'])
503
638
 
504
639
  save_data = action['return']
505
640
  action_name = action['action']
641
+ batch_action = action.get("batch_action", False)
642
+
506
643
  next_action = self._get_next_action(stype, index)
507
644
  # print(args)
508
645
  if instrument == 'if':
@@ -510,16 +647,29 @@ class Script(db.Model):
510
647
  elif instrument == 'while':
511
648
  return self._process_while(indent_unit, action_name, statement, next_action)
512
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
+
513
653
  return self.indent(indent_unit) + f"{action_name} = {statement}", indent_unit
514
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
515
657
  return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
516
658
  elif instrument == 'repeat':
659
+
517
660
  return self._process_repeat(indent_unit, action_name, statement, next_action)
518
- #todo
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
519
667
  # elif instrument == 'registered_workflows':
520
668
  # return inspect.getsource(my_function)
521
669
  else:
522
- return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data)
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)
523
673
 
524
674
  def _process_args(self, args):
525
675
  """
@@ -579,23 +729,44 @@ class Script(db.Model):
579
729
  indent_unit -= 1
580
730
  return exec_string, indent_unit
581
731
 
582
- 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):
583
734
  """
584
735
  Process actions related to instruments.
585
736
  """
737
+ async_str = "await " if is_async else ""
586
738
 
587
- if isinstance(args, dict):
739
+ function_call = f"{instrument}.{action}"
740
+ if instrument.startswith("blocks"):
741
+ self.blocks_included = True
742
+ function_call = action
743
+
744
+ if isinstance(args, dict) and args != {}:
588
745
  args_str = self._process_dict_args(args)
589
- single_line = f"{instrument}.{action}(**{args_str})"
746
+ single_line = f"{async_str}{function_call}(**{args_str})"
590
747
  elif isinstance(args, str):
591
- single_line = f"{instrument}.{action} = {args}"
748
+ single_line = f"{function_call} = {args}"
592
749
  else:
593
- single_line = f"{instrument}.{action}()"
750
+ single_line = f"{async_str}{function_call}()"
751
+
594
752
 
595
- if save_data:
596
- save_data += " = "
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
597
768
 
598
- return self.indent(indent_unit) + save_data + single_line, indent_unit
769
+ return output_code, indent_unit
599
770
 
600
771
  def _process_dict_args(self, args):
601
772
  """
@@ -607,12 +778,21 @@ class Script(db.Model):
607
778
  args_str = args_str.replace(f"'#{args[arg][1:]}'", args[arg][1:])
608
779
  elif isinstance(args[arg], dict):
609
780
  # print(args[arg])
610
- variables = self.get_variables()
781
+ if not args[arg]:
782
+ continue
783
+ # Extract the variable name (first key in the dict)
611
784
  value = next(iter(args[arg]))
612
- if value not in variables:
613
- raise ValueError(f"Variable ({value}) is not defined.")
614
- args_str = args_str.replace(f"{args[arg]}", next(iter(args[arg])))
615
- # elif self._is_variable(arg):
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):
616
796
  # print("is variable")
617
797
  # args_str = args_str.replace(f"'{args[arg]}'", args[arg])
618
798
  return args_str
@@ -631,7 +811,7 @@ class Script(db.Model):
631
811
  """
632
812
  return arg in self.script_dict and self.script_dict[arg].get("arg_types") == "variable"
633
813
 
634
- 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):
635
815
  """
636
816
  Write the compiled script to a file.
637
817
  """
@@ -641,10 +821,30 @@ class Script(db.Model):
641
821
  else:
642
822
  s.write("deck = None")
643
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
+
644
829
  for i in exec_string.values():
645
830
  s.write(f"\n\n\n{i}")
646
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
+
647
846
  class WorkflowRun(db.Model):
847
+ """Represents the entire experiment"""
648
848
  __tablename__ = 'workflow_runs'
649
849
 
650
850
  id = db.Column(db.Integer, primary_key=True)
@@ -653,30 +853,65 @@ class WorkflowRun(db.Model):
653
853
  start_time = db.Column(db.DateTime, default=datetime.now())
654
854
  end_time = db.Column(db.DateTime)
655
855
  data_path = db.Column(db.String(256))
656
- steps = db.relationship(
657
- 'WorkflowStep',
658
- backref='workflow_runs',
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
659
862
  cascade='all, delete-orphan',
660
- passive_deletes=True
863
+ lazy='dynamic' # Good for handling many iterations
661
864
  )
662
865
  def as_dict(self):
663
866
  dict = self.__dict__
664
867
  dict.pop('_sa_instance_state', None)
665
868
  return dict
666
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
+
667
899
  class WorkflowStep(db.Model):
668
900
  __tablename__ = 'workflow_steps'
669
901
 
670
902
  id = db.Column(db.Integer, primary_key=True)
671
- workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
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)
672
905
 
673
- phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
674
- 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
675
908
  step_index = db.Column(db.Integer, default=0)
676
909
  method_name = db.Column(db.String(128), nullable=False)
677
910
  start_time = db.Column(db.DateTime)
678
911
  end_time = db.Column(db.DateTime)
679
912
  run_error = db.Column(db.Boolean, default=False)
913
+ output = db.Column(JSONType, default={})
914
+ # Using as_dict method from ModelBase
680
915
 
681
916
  def as_dict(self):
682
917
  dict = self.__dict__.copy()
@@ -693,7 +928,7 @@ class SingleStep(db.Model):
693
928
  start_time = db.Column(db.DateTime)
694
929
  end_time = db.Column(db.DateTime)
695
930
  run_error = db.Column(db.String(128))
696
- output = db.Column(JSONType)
931
+ output = db.Column(JSONType, nullable=True)
697
932
 
698
933
  def as_dict(self):
699
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"])