ivoryos 1.2.7__py3-none-any.whl → 1.3.0__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.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

ivoryos/server.py ADDED
@@ -0,0 +1,168 @@
1
+ import os
2
+ import sqlite3
3
+ import sys
4
+ from typing import Union
5
+
6
+ from flask import Blueprint
7
+
8
+ from sqlalchemy import Engine, event
9
+
10
+ # from ivoryos import BUILDING_BLOCKS
11
+ from ivoryos.app import create_app
12
+ from ivoryos.config import Config, get_config
13
+ from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
14
+ from ivoryos.routes.auth.auth import login_manager
15
+ from ivoryos.routes.control.control import global_config
16
+ from ivoryos.socket_handlers import socketio
17
+ from ivoryos.utils import utils
18
+ from ivoryos.utils.db_models import db, User
19
+
20
+
21
+ url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
22
+
23
+ @event.listens_for(Engine, "connect")
24
+ def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
25
+ if isinstance(dbapi_connection, sqlite3.Connection):
26
+ cursor = dbapi_connection.cursor()
27
+ cursor.execute("PRAGMA foreign_keys=ON")
28
+ cursor.close()
29
+
30
+
31
+
32
+ @login_manager.user_loader
33
+ def load_user(user_id):
34
+ """
35
+ This function is called by Flask-Login on every request to get the
36
+ current user object from the user ID stored in the session.
37
+ """
38
+ # The correct implementation is to fetch the user from the database.
39
+ return db.session.get(User, user_id)
40
+
41
+
42
+
43
+
44
+
45
+ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, model=None,
46
+ config: Config = None,
47
+ logger: Union[str, list] = None,
48
+ logger_output_name: str = None,
49
+ enable_design: bool = True,
50
+ blueprint_plugins: Union[list, Blueprint] = [],
51
+ exclude_names: list = [],
52
+ ):
53
+ """
54
+ Start ivoryOS app server.
55
+
56
+ :param module: module name, __name__ for current module
57
+ :param host: host address, defaults to 0.0.0.0
58
+ :param port: port, defaults to None, and will use 8000
59
+ :param debug: debug mode, defaults to None (True)
60
+ :param llm_server: llm server, defaults to None.
61
+ :param model: llm model, defaults to None. If None, app will run without text-to-code feature
62
+ :param config: config class, defaults to None
63
+ :param logger: logger name of list of logger names, defaults to None
64
+ :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
65
+ :param enable_design: enable design canvas, database and workflow execution
66
+ :param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
67
+ :param exclude_names: list[str] module names to exclude from parsing
68
+ """
69
+ app = create_app(config_class=config or get_config()) # Create app instance using factory function
70
+
71
+ # plugins = load_installed_plugins(app, socketio)
72
+ plugins = []
73
+ if blueprint_plugins:
74
+ config_plugins = load_plugins(blueprint_plugins, app, socketio)
75
+ plugins.extend(config_plugins)
76
+
77
+ def inject_nav_config():
78
+ """Make NAV_CONFIG available globally to all templates."""
79
+ return dict(
80
+ enable_design=enable_design,
81
+ plugins=plugins,
82
+ )
83
+
84
+ app.context_processor(inject_nav_config)
85
+ port = port or int(os.environ.get("PORT", 8000))
86
+ debug = debug if debug is not None else app.config.get('DEBUG', True)
87
+
88
+ app.config["LOGGERS"] = logger
89
+ app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
90
+ logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
91
+ dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
92
+ global_config.optimizers = OPTIMIZER_REGISTRY
93
+ if module:
94
+ app.config["MODULE"] = module
95
+ app.config["OFF_LINE"] = False
96
+ global_config.deck = sys.modules[module]
97
+ global_config.building_blocks = utils.create_block_snapshot()
98
+ global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
99
+ output_path=dummy_deck_path,
100
+ save=True,
101
+ exclude_names=exclude_names
102
+ )
103
+
104
+ else:
105
+ app.config["OFF_LINE"] = True
106
+ if model:
107
+ app.config["ENABLE_LLM"] = True
108
+ app.config["LLM_MODEL"] = model
109
+ app.config["LLM_SERVER"] = llm_server
110
+ utils.install_and_import('openai')
111
+ from ivoryos.utils.llm_agent import LlmAgent
112
+ global_config.agent = LlmAgent(host=llm_server, model=model,
113
+ output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
114
+ else:
115
+ app.config["ENABLE_LLM"] = False
116
+ if logger and type(logger) is str:
117
+ utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
118
+ elif type(logger) is list:
119
+ for log in logger:
120
+ utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
121
+
122
+ # TODO in case Python 3.12 or higher doesn't log URL
123
+ # if sys.version_info >= (3, 12):
124
+ # ip = utils.get_local_ip()
125
+ # print(f"Server running at http://localhost:{port}")
126
+ # if not ip == "127.0.0.1":
127
+ # print(f"Server running at http://{ip}:{port}")
128
+ socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
129
+ # return app
130
+
131
+
132
+ # def load_installed_plugins(app, socketio):
133
+ # """
134
+ # Dynamically load installed plugins and attach Flask-SocketIO.
135
+ # """
136
+ # plugin_names = []
137
+ # for entry_point in entry_points().get("ivoryos.plugins", []):
138
+ # plugin = entry_point.load()
139
+ #
140
+ # # If the plugin has an `init_socketio()` function, pass socketio
141
+ # if hasattr(plugin, 'init_socketio'):
142
+ # plugin.init_socketio(socketio)
143
+ #
144
+ # plugin_names.append(entry_point.name)
145
+ # app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
146
+ #
147
+ # return plugin_names
148
+
149
+
150
+ def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
151
+ """
152
+ Dynamically load installed plugins and attach Flask-SocketIO.
153
+ :param blueprints: Union[list, Blueprint] list of Blueprint objects or a single Blueprint object
154
+ :param app: Flask application instance
155
+ :param socketio: Flask-SocketIO instance
156
+ :return: list of plugin names
157
+ """
158
+ plugin_names = []
159
+ if not isinstance(blueprints, list):
160
+ blueprints = [blueprints]
161
+ for blueprint in blueprints:
162
+ # If the plugin has an `init_socketio()` function, pass socketio
163
+ if hasattr(blueprint, 'init_socketio'):
164
+ blueprint.init_socketio(socketio)
165
+ plugin_names.append(blueprint.name)
166
+ app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
167
+ return plugin_names
168
+
@@ -37,13 +37,43 @@ document.addEventListener("DOMContentLoaded", function() {
37
37
  console.error("Error received:", errorData);
38
38
  var progressBar = document.getElementById('progress-bar-inner');
39
39
 
40
- progressBar.classList.remove('bg-success');
41
- progressBar.classList.add('bg-danger'); // Red color for error
42
- // Show error modal
40
+ progressBar.classList.remove('bg-success', 'bg-warning');
41
+ progressBar.classList.add('bg-danger');
42
+
43
43
  var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
44
- document.getElementById('error-message').innerText = "An error occurred: " + errorData.message;
44
+ document.getElementById('errorModalLabel').innerText = "Error Detected";
45
+ document.getElementById('error-message').innerText =
46
+ "An error occurred: " + errorData.message;
47
+
48
+ // Show all buttons again
49
+ document.getElementById('retry-btn').style.display = "inline-block";
50
+ document.getElementById('continue-btn').style.display = "inline-block";
51
+ document.getElementById('stop-btn').style.display = "inline-block";
52
+
45
53
  errorModal.show();
54
+ });
55
+
56
+
57
+ socket.on('human_intervention', function(data) {
58
+ console.warn("Human intervention required:", data);
59
+ var progressBar = document.getElementById('progress-bar-inner');
46
60
 
61
+ // Set progress bar to yellow
62
+ progressBar.classList.remove('bg-success', 'bg-danger');
63
+ progressBar.classList.add('bg-warning');
64
+
65
+ // Reuse error modal but update content
66
+ var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
67
+ document.getElementById('errorModalLabel').innerText = "Human Intervention Required";
68
+ document.getElementById('error-message').innerText =
69
+ "Workflow paused: " + (data.message || "Please check and manually resume.");
70
+
71
+ // Optionally: hide retry button, since it may not apply
72
+ document.getElementById('retry-btn').style.display = "none";
73
+ document.getElementById('continue-btn').style.display = "inline-block";
74
+ document.getElementById('stop-btn').style.display = "inline-block";
75
+
76
+ errorModal.show();
47
77
  });
48
78
 
49
79
  // Handle Pause/Resume Button
@@ -71,6 +101,11 @@ document.addEventListener("DOMContentLoaded", function() {
71
101
  document.getElementById('continue-btn').addEventListener('click', function() {
72
102
  socket.emit('pause'); // Resume execution
73
103
  console.log("Execution resumed.");
104
+
105
+ // Reset progress bar color to running (blue)
106
+ var progressBar = document.getElementById('progress-bar-inner');
107
+ progressBar.classList.remove('bg-danger', 'bg-warning');
108
+ progressBar.classList.add('bg-primary');
74
109
  });
75
110
 
76
111
  document.getElementById('retry-btn').addEventListener('click', function() {
@@ -115,20 +115,37 @@ function insertDropPlaceholder($target) {
115
115
 
116
116
  // Add this function to sortable_design.js
117
117
  function initializeDragHandlers() {
118
- $(".accordion-item").off("dragstart").on("dragstart", function (event) {
119
- let formHtml = $(this).find(".accordion-body form").prop('outerHTML');
120
-
121
- if (!formHtml) {
122
- console.error("Form not found in accordion-body");
123
- return false;
124
- }
118
+ const $cards = $(".accordion-item.design-control");
125
119
 
126
- event.originalEvent.dataTransfer.setData("form", formHtml);
127
- event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
128
- event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
120
+ // Toggle draggable based on mouse/touch position
121
+ $cards.off("mousedown touchstart").on("mousedown touchstart", function (event) {
122
+ this.setAttribute("draggable", $(event.target).closest(".input-group").length ? "false" : "true");
123
+ });
129
124
 
130
- $(this).addClass("dragging");
125
+ // Handle the actual drag
126
+ $cards.off("dragstart dragend").on({
127
+ dragstart: function (event) {
128
+ if (this.getAttribute("draggable") !== "true") {
129
+ event.preventDefault();
130
+ return false;
131
+ }
132
+
133
+ const formHtml = $(this).find(".accordion-body form").prop("outerHTML");
134
+ if (!formHtml) return false;
135
+
136
+ event.originalEvent.dataTransfer.setData("form", formHtml);
137
+ event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
138
+ event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
139
+
140
+ $(this).addClass("dragging");
141
+ },
142
+ dragend: function () {
143
+ $(this).removeClass("dragging").attr("draggable", "false");
144
+ }
131
145
  });
146
+
147
+ // Prevent form inputs from being draggable
148
+ $(".accordion-item input, .accordion-item select").attr("draggable", "false");
132
149
  }
133
150
 
134
151
  // Make sure it's called in the document ready function
@@ -318,6 +318,12 @@ class Script(db.Model):
318
318
  {"id": current_len + 2, "instrument": 'repeat', "action": 'endrepeat',
319
319
  "args": {}, "return": '', "uuid": uid},
320
320
  ],
321
+ "pause":
322
+ [
323
+ {"id": current_len + 1, "instrument": 'pause', "action": "pause",
324
+ "args": {"statement": 1 if statement == '' else statement}, "return": '', "uuid": uid,
325
+ "arg_types": {"statement": "str"}}
326
+ ],
321
327
  }
322
328
  action_list = logic_dict[logic_type]
323
329
  self.currently_editing_script.extend(action_list)
@@ -443,6 +449,9 @@ class Script(db.Model):
443
449
  Compile the current script to a Python file.
444
450
  :return: String to write to a Python file.
445
451
  """
452
+ self.needs_call_human = False
453
+ self.blocks_included = False
454
+
446
455
  self.sort_actions()
447
456
  run_name = self.name if self.name else "untitled"
448
457
  run_name = self.validate_function_name(run_name)
@@ -524,6 +533,9 @@ class Script(db.Model):
524
533
  return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
525
534
  elif instrument == 'repeat':
526
535
  return self._process_repeat(indent_unit, action_name, statement, next_action)
536
+ elif instrument == 'pause':
537
+ self.needs_call_human = True
538
+ return f"{self.indent(indent_unit)}pause('{statement}')", indent_unit
527
539
  #todo
528
540
  # elif instrument == 'registered_workflows':
529
541
  # return inspect.getsource(my_function)
@@ -592,14 +604,18 @@ class Script(db.Model):
592
604
  """
593
605
  Process actions related to instruments.
594
606
  """
607
+ function_call = f"{instrument}.{action}"
608
+ if instrument.startswith("blocks"):
609
+ self.blocks_included = True
610
+ function_call = action
595
611
 
596
- if isinstance(args, dict):
612
+ if isinstance(args, dict) and args != {}:
597
613
  args_str = self._process_dict_args(args)
598
- single_line = f"{instrument}.{action}(**{args_str})"
614
+ single_line = f"{function_call}(**{args_str})"
599
615
  elif isinstance(args, str):
600
- single_line = f"{instrument}.{action} = {args}"
616
+ single_line = f"{function_call} = {args}"
601
617
  else:
602
- single_line = f"{instrument}.{action}()"
618
+ single_line = f"{function_call}()"
603
619
 
604
620
  if save_data:
605
621
  save_data += " = "
@@ -640,7 +656,7 @@ class Script(db.Model):
640
656
  """
641
657
  return arg in self.script_dict and self.script_dict[arg].get("arg_types") == "variable"
642
658
 
643
- def _write_to_file(self, script_path, run_name, exec_string):
659
+ def _write_to_file(self, script_path, run_name, exec_string, call_human=False):
644
660
  """
645
661
  Write the compiled script to a file.
646
662
  """
@@ -650,10 +666,30 @@ class Script(db.Model):
650
666
  else:
651
667
  s.write("deck = None")
652
668
  s.write("\nimport time")
669
+ if self.blocks_included:
670
+ s.write(f"\n{self._create_block_import()}")
671
+ if self.needs_call_human:
672
+ s.write("""\n\ndef pause(reason="Manual intervention required"):\n\tprint(f"\\nHUMAN INTERVENTION REQUIRED: {reason}")\n\tinput("Press Enter to continue...\\n")""")
673
+
653
674
  for i in exec_string.values():
654
675
  s.write(f"\n\n\n{i}")
655
676
 
677
+ def _create_block_import(self):
678
+ imports = {}
679
+ from ivoryos.utils.decorators import BUILDING_BLOCKS
680
+ for category, methods in BUILDING_BLOCKS.items():
681
+ for method_name, meta in methods.items():
682
+ func = meta["func"]
683
+ module = meta["path"]
684
+ name = func.__name__
685
+ imports.setdefault(module, set()).add(name)
686
+ lines = []
687
+ for module, funcs in imports.items():
688
+ lines.append(f"from {module} import {', '.join(sorted(funcs))}")
689
+ return "\n".join(lines)
690
+
656
691
  class WorkflowRun(db.Model):
692
+ """Represents the entire experiment"""
657
693
  __tablename__ = 'workflow_runs'
658
694
 
659
695
  id = db.Column(db.Integer, primary_key=True)
@@ -662,30 +698,65 @@ class WorkflowRun(db.Model):
662
698
  start_time = db.Column(db.DateTime, default=datetime.now())
663
699
  end_time = db.Column(db.DateTime)
664
700
  data_path = db.Column(db.String(256))
665
- steps = db.relationship(
666
- 'WorkflowStep',
667
- backref='workflow_runs',
701
+ repeat_mode = db.Column(db.String(64), default="none") # static_repeat, sweep, optimizer
702
+
703
+ # A run contains multiple iterations
704
+ phases = db.relationship(
705
+ 'WorkflowPhase',
706
+ backref='workflow_runs', # Clearer back-reference name
668
707
  cascade='all, delete-orphan',
669
- passive_deletes=True
708
+ lazy='dynamic' # Good for handling many iterations
670
709
  )
671
710
  def as_dict(self):
672
711
  dict = self.__dict__
673
712
  dict.pop('_sa_instance_state', None)
674
713
  return dict
675
714
 
715
+ class WorkflowPhase(db.Model):
716
+ """Represents a single function call within a WorkflowRun."""
717
+ __tablename__ = 'workflow_phases'
718
+
719
+ id = db.Column(db.Integer, primary_key=True)
720
+ # Foreign key to link this iteration to its parent run
721
+ run_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
722
+
723
+ # NEW: Store iteration-specific parameters here
724
+ name = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
725
+ repeat_index = db.Column(db.Integer, default=0)
726
+
727
+ parameters = db.Column(JSONType) # Use db.JSON for general support
728
+ outputs = db.Column(JSONType)
729
+ start_time = db.Column(db.DateTime, default=datetime.now)
730
+ end_time = db.Column(db.DateTime)
731
+
732
+ # An iteration contains multiple steps
733
+ steps = db.relationship(
734
+ 'WorkflowStep',
735
+ backref='workflow_phases', # Clearer back-reference name
736
+ cascade='all, delete-orphan'
737
+ )
738
+
739
+ def as_dict(self):
740
+ dict = self.__dict__.copy()
741
+ dict.pop('_sa_instance_state', None)
742
+ return dict
743
+
676
744
  class WorkflowStep(db.Model):
677
745
  __tablename__ = 'workflow_steps'
678
746
 
679
747
  id = db.Column(db.Integer, primary_key=True)
680
- workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
748
+ # workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=True)
749
+ phase_id = db.Column(db.Integer, db.ForeignKey('workflow_phases.id', ondelete='CASCADE'), nullable=True)
681
750
 
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
751
+ # phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
752
+ # repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
684
753
  step_index = db.Column(db.Integer, default=0)
685
754
  method_name = db.Column(db.String(128), nullable=False)
686
755
  start_time = db.Column(db.DateTime)
687
756
  end_time = db.Column(db.DateTime)
688
757
  run_error = db.Column(db.Boolean, default=False)
758
+ output = db.Column(JSONType, default={})
759
+ # Using as_dict method from ModelBase
689
760
 
690
761
  def as_dict(self):
691
762
  dict = self.__dict__.copy()
@@ -702,7 +773,7 @@ class SingleStep(db.Model):
702
773
  start_time = db.Column(db.DateTime)
703
774
  end_time = db.Column(db.DateTime)
704
775
  run_error = db.Column(db.String(128))
705
- output = db.Column(JSONType)
776
+ output = db.Column(JSONType, nullable=True)
706
777
 
707
778
  def as_dict(self):
708
779
  dict = self.__dict__.copy()
@@ -0,0 +1,33 @@
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
+ "path": module
20
+ }
21
+ return func
22
+ if _func is None:
23
+ return decorator
24
+ else:
25
+ return decorator(_func)
26
+
27
+
28
+ class BlockNamespace:
29
+ """[not in use] Expose methods for one block category as attributes."""
30
+
31
+ def __init__(self, methods):
32
+ for name, meta in methods.items():
33
+ setattr(self, name, meta["func"])
ivoryos/utils/form.py CHANGED
@@ -210,10 +210,10 @@ class FlexibleEnumField(StringField):
210
210
  if key in self.choices:
211
211
  # Convert the string key to Enum instance
212
212
  self.data = self.enum_class[key].value
213
- elif self.data.startswith("#"):
213
+ elif key.startswith("#"):
214
214
  if not self.script.editing_type == "script":
215
215
  raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
216
- self.data = self.data
216
+ self.data = key
217
217
  else:
218
218
  raise ValidationError(
219
219
  f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
@@ -286,7 +286,9 @@ def create_form_for_method(method, autofill, script=None, design=True):
286
286
  # enum_class = [(e.name, e.value) for e in param.annotation]
287
287
  field_class = FlexibleEnumField
288
288
  placeholder_text = f"Choose or type a value for {param.annotation.__name__} (start with # for custom)"
289
+
289
290
  extra_kwargs = {"choices": param.annotation}
291
+
290
292
  else:
291
293
  # print(param.annotation)
292
294
  annotation, optional = parse_annotation(param.annotation)
@@ -429,7 +431,7 @@ def create_form_from_action(action: dict, script=None, design=True):
429
431
 
430
432
  def create_all_builtin_forms(script):
431
433
  all_builtin_forms = {}
432
- for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
434
+ for logic_name in ['if', 'while', 'variable', 'wait', 'repeat', 'pause']:
433
435
  # signature = info.get('signature', {})
434
436
  form_class = create_builtin_form(logic_name, script)
435
437
  all_builtin_forms[logic_name] = form_class()
@@ -444,7 +446,8 @@ def create_builtin_form(logic_type, script):
444
446
 
445
447
  placeholder_text = {
446
448
  'wait': 'Enter second',
447
- 'repeat': 'Enter an integer'
449
+ 'repeat': 'Enter an integer',
450
+ 'pause': 'Human Intervention Message'
448
451
  }.get(logic_type, 'Enter statement')
449
452
  description_text = {
450
453
  'variable': 'Your variable can be numbers, boolean (True or False) or text ("text")',
@@ -536,6 +539,7 @@ def _action_button(action: dict, variables: dict):
536
539
  "repeat": "background-color: lightsteelblue",
537
540
  "if": "background-color: salmon",
538
541
  "while": "background-color: salmon",
542
+ "pause": "background-color: goldenrod",
539
543
  }.get(action['instrument'], "")
540
544
 
541
545
  if action['instrument'] in ['if', 'while', 'repeat']:
@@ -8,6 +8,7 @@ class GlobalConfig:
8
8
  if cls._instance is None:
9
9
  cls._instance = super(GlobalConfig, cls).__new__(cls, *args, **kwargs)
10
10
  cls._instance._deck = None
11
+ cls._instance._building_blocks = None
11
12
  cls._instance._registered_workflows = None
12
13
  cls._instance._agent = None
13
14
  cls._instance._defined_variables = {}
@@ -27,6 +28,15 @@ class GlobalConfig:
27
28
  if self._deck is None:
28
29
  self._deck = value
29
30
 
31
+ @property
32
+ def building_blocks(self):
33
+ return self._building_blocks
34
+
35
+ @building_blocks.setter
36
+ def building_blocks(self, value):
37
+ if self._building_blocks is None:
38
+ self._building_blocks = value
39
+
30
40
  @property
31
41
  def registered_workflows(self):
32
42
  return self._registered_workflows