ivoryos 1.2.7__py3-none-any.whl → 1.2.8__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/__init__.py CHANGED
@@ -1,248 +1,13 @@
1
- import os
2
- import sys
3
- import uuid
4
- from typing import Union
5
-
6
- from flask import Flask, redirect, url_for, g, Blueprint, session
7
- from flask_login import AnonymousUserMixin
8
-
9
- # sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
-
11
- from ivoryos.config import Config, get_config
12
- from ivoryos.routes.auth.auth import auth, login_manager
13
- from ivoryos.routes.control.control import control
14
- from ivoryos.routes.data.data import data
15
- from ivoryos.routes.library.library import library
16
- from ivoryos.routes.design.design import design
17
- from ivoryos.routes.execute.execute import execute
18
- from ivoryos.routes.api.api import api
19
- from ivoryos.socket_handlers import socketio
20
- from ivoryos.routes.main.main import main
21
- # from ivoryos.routes.monitor.monitor import monitor
22
- from ivoryos.utils import utils
23
- from ivoryos.utils.db_models import db, User
24
- from ivoryos.utils.global_config import GlobalConfig
1
+ from ivoryos.server import run
25
2
  from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
26
- from ivoryos.utils.script_runner import ScriptRunner
27
3
  from ivoryos.version import __version__ as ivoryos_version
28
- # from importlib.metadata import entry_points
29
-
30
- global_config = GlobalConfig()
31
- from sqlalchemy import event
32
- from sqlalchemy.engine import Engine
33
- import sqlite3
34
-
35
-
36
- @event.listens_for(Engine, "connect")
37
- def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
38
- if isinstance(dbapi_connection, sqlite3.Connection):
39
- cursor = dbapi_connection.cursor()
40
- cursor.execute("PRAGMA foreign_keys=ON")
41
- cursor.close()
42
-
43
-
44
- url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
45
- app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
46
- app.register_blueprint(main, url_prefix=url_prefix)
47
- app.register_blueprint(auth, url_prefix=f'{url_prefix}/{auth.name}')
48
- app.register_blueprint(library, url_prefix=f'{url_prefix}/{library.name}')
49
- app.register_blueprint(control, url_prefix=f'{url_prefix}/instruments')
50
- app.register_blueprint(design, url_prefix=f'{url_prefix}')
51
- app.register_blueprint(execute, url_prefix=f'{url_prefix}')
52
- app.register_blueprint(data, url_prefix=f'{url_prefix}')
53
- app.register_blueprint(api, url_prefix=f'{url_prefix}/{api.name}')
54
-
55
- @login_manager.user_loader
56
- def load_user(user_id):
57
- """
58
- This function is called by Flask-Login on every request to get the
59
- current user object from the user ID stored in the session.
60
- """
61
- # The correct implementation is to fetch the user from the database.
62
- return db.session.get(User, user_id)
63
-
64
-
65
- def create_app(config_class=None):
66
- """
67
- create app, init database
68
- """
69
- app.config.from_object(config_class or 'config.get_config()')
70
- os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True)
71
- # Initialize extensions
72
- socketio.init_app(app, cors_allowed_origins="*", cookie=None)
73
- login_manager.init_app(app)
74
- login_manager.login_view = "auth.login"
75
- db.init_app(app)
76
-
77
- # Create database tables
78
- with app.app_context():
79
- db.create_all()
80
-
81
- # Additional setup
82
- utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
83
-
84
- # logger_list = app.config["LOGGERS"]
85
- logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
86
- logger = utils.start_logger(socketio, 'gui_logger', logger_path)
87
-
88
- @app.before_request
89
- def before_request():
90
- """
91
- Called before
92
-
93
- """
94
- g.logger = logger
95
- g.socketio = socketio
96
- session.permanent = False
97
- # DEMO_MODE: Simulate logged-in user per session
98
- if app.config.get("DEMO_MODE", False):
99
- if "demo_user_id" not in session:
100
- session["demo_user_id"] = f"demo_{str(uuid.uuid4())[:8]}"
101
-
102
- class SessionDemoUser(AnonymousUserMixin):
103
- @property
104
- def is_authenticated(self):
105
- return True
106
-
107
- def get_id(self):
108
- return session.get("demo_user_id")
109
-
110
- login_manager.anonymous_user = SessionDemoUser
111
-
112
-
113
-
114
- @app.route('/')
115
- def redirect_to_prefix():
116
- return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
117
-
118
- @app.template_filter('format_name')
119
- def format_name(name):
120
- name = name.split(".")[-1]
121
- text = ' '.join(word for word in name.split('_'))
122
- return text.capitalize()
123
-
124
- # app.config.setdefault("DEMO_MODE", False)
125
- return app
126
-
127
-
128
- def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, model=None,
129
- config: Config = None,
130
- logger: Union[str, list] = None,
131
- logger_output_name: str = None,
132
- enable_design: bool = True,
133
- blueprint_plugins: Union[list, Blueprint] = [],
134
- exclude_names: list = [],
135
- ):
136
- """
137
- Start ivoryOS app server.
138
-
139
- :param module: module name, __name__ for current module
140
- :param host: host address, defaults to 0.0.0.0
141
- :param port: port, defaults to None, and will use 8000
142
- :param debug: debug mode, defaults to None (True)
143
- :param llm_server: llm server, defaults to None.
144
- :param model: llm model, defaults to None. If None, app will run without text-to-code feature
145
- :param config: config class, defaults to None
146
- :param logger: logger name of list of logger names, defaults to None
147
- :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
148
- :param enable_design: enable design canvas, database and workflow execution
149
- :param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
150
- :param exclude_names: list[str] module names to exclude from parsing
151
- """
152
- app = create_app(config_class=config or get_config()) # Create app instance using factory function
153
-
154
- # plugins = load_installed_plugins(app, socketio)
155
- plugins = []
156
- if blueprint_plugins:
157
- config_plugins = load_plugins(blueprint_plugins, app, socketio)
158
- plugins.extend(config_plugins)
159
-
160
- def inject_nav_config():
161
- """Make NAV_CONFIG available globally to all templates."""
162
- return dict(
163
- enable_design=enable_design,
164
- plugins=plugins,
165
- )
166
-
167
- app.context_processor(inject_nav_config)
168
- port = port or int(os.environ.get("PORT", 8000))
169
- debug = debug if debug is not None else app.config.get('DEBUG', True)
170
-
171
- app.config["LOGGERS"] = logger
172
- app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
173
- logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
174
- dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
175
- global_config.optimizers = OPTIMIZER_REGISTRY
176
- if module:
177
- app.config["MODULE"] = module
178
- app.config["OFF_LINE"] = False
179
- global_config.deck = sys.modules[module]
180
- global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
181
- output_path=dummy_deck_path,
182
- save=True,
183
- exclude_names=exclude_names
184
- )
185
- else:
186
- app.config["OFF_LINE"] = True
187
- if model:
188
- app.config["ENABLE_LLM"] = True
189
- app.config["LLM_MODEL"] = model
190
- app.config["LLM_SERVER"] = llm_server
191
- utils.install_and_import('openai')
192
- from ivoryos.utils.llm_agent import LlmAgent
193
- global_config.agent = LlmAgent(host=llm_server, model=model,
194
- output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
195
- else:
196
- app.config["ENABLE_LLM"] = False
197
- if logger and type(logger) is str:
198
- utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
199
- elif type(logger) is list:
200
- for log in logger:
201
- utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
202
-
203
- # in case Python 3.12 or higher doesn't log URL
204
- if sys.version_info >= (3, 12):
205
- ip = utils.get_local_ip()
206
- print(f"Server running at http://localhost:{port}")
207
- if not ip == "127.0.0.1":
208
- print(f"Server running at http://{ip}:{port}")
209
- socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
210
- # return app
211
-
212
-
213
- # def load_installed_plugins(app, socketio):
214
- # """
215
- # Dynamically load installed plugins and attach Flask-SocketIO.
216
- # """
217
- # plugin_names = []
218
- # for entry_point in entry_points().get("ivoryos.plugins", []):
219
- # plugin = entry_point.load()
220
- #
221
- # # If the plugin has an `init_socketio()` function, pass socketio
222
- # if hasattr(plugin, 'init_socketio'):
223
- # plugin.init_socketio(socketio)
224
- #
225
- # plugin_names.append(entry_point.name)
226
- # app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
227
- #
228
- # return plugin_names
4
+ from ivoryos.utils.decorators import block, BUILDING_BLOCKS
229
5
 
230
6
 
231
- def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
232
- """
233
- Dynamically load installed plugins and attach Flask-SocketIO.
234
- :param blueprints: Union[list, Blueprint] list of Blueprint objects or a single Blueprint object
235
- :param app: Flask application instance
236
- :param socketio: Flask-SocketIO instance
237
- :return: list of plugin names
238
- """
239
- plugin_names = []
240
- if not isinstance(blueprints, list):
241
- blueprints = [blueprints]
242
- for blueprint in blueprints:
243
- # If the plugin has an `init_socketio()` function, pass socketio
244
- if hasattr(blueprint, 'init_socketio'):
245
- blueprint.init_socketio(socketio)
246
- plugin_names.append(blueprint.name)
247
- app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
248
- return plugin_names
7
+ __all__ = [
8
+ "block",
9
+ "BUILDING_BLOCKS",
10
+ "OPTIMIZER_REGISTRY",
11
+ "run",
12
+ "ivoryos_version",
13
+ ]
ivoryos/app.py ADDED
@@ -0,0 +1,94 @@
1
+ import os
2
+ import uuid
3
+
4
+ from flask import Flask, session, g, redirect, url_for
5
+ from flask_login import AnonymousUserMixin
6
+
7
+ from ivoryos.utils import utils
8
+ from ivoryos.utils.db_models import db
9
+ from ivoryos.config import Config, get_config
10
+ from ivoryos.routes.auth.auth import auth, login_manager
11
+ from ivoryos.routes.control.control import control
12
+ from ivoryos.routes.data.data import data
13
+ from ivoryos.routes.library.library import library
14
+ from ivoryos.routes.design.design import design
15
+ from ivoryos.routes.execute.execute import execute
16
+ from ivoryos.routes.api.api import api
17
+ from ivoryos.socket_handlers import socketio
18
+ from ivoryos.routes.main.main import main
19
+ from ivoryos.version import __version__ as ivoryos_version
20
+
21
+ def create_app(config_class=None):
22
+ """
23
+ create app, init database
24
+ """
25
+
26
+ url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
27
+ app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
28
+ app.register_blueprint(main, url_prefix=url_prefix)
29
+ app.register_blueprint(auth, url_prefix=f'{url_prefix}/{auth.name}')
30
+ app.register_blueprint(library, url_prefix=f'{url_prefix}/{library.name}')
31
+ app.register_blueprint(control, url_prefix=f'{url_prefix}/instruments')
32
+ app.register_blueprint(design, url_prefix=f'{url_prefix}')
33
+ app.register_blueprint(execute, url_prefix=f'{url_prefix}')
34
+ app.register_blueprint(data, url_prefix=f'{url_prefix}')
35
+ app.register_blueprint(api, url_prefix=f'{url_prefix}/{api.name}')
36
+
37
+
38
+ app.config.from_object(config_class or 'config.get_config()')
39
+ os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True)
40
+ # Initialize extensions
41
+ socketio.init_app(app, cors_allowed_origins="*", cookie=None)
42
+ login_manager.init_app(app)
43
+ login_manager.login_view = "auth.login"
44
+ db.init_app(app)
45
+
46
+ # Create database tables
47
+ with app.app_context():
48
+ db.create_all()
49
+
50
+ # Additional setup
51
+ utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
52
+
53
+ # logger_list = app.config["LOGGERS"]
54
+ logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
55
+ logger = utils.start_logger(socketio, 'gui_logger', logger_path)
56
+
57
+ @app.before_request
58
+ def before_request():
59
+ """
60
+ Called before
61
+
62
+ """
63
+ g.logger = logger
64
+ g.socketio = socketio
65
+ session.permanent = False
66
+ # DEMO_MODE: Simulate logged-in user per session
67
+ if app.config.get("DEMO_MODE", False):
68
+ if "demo_user_id" not in session:
69
+ session["demo_user_id"] = f"demo_{str(uuid.uuid4())[:8]}"
70
+
71
+ class SessionDemoUser(AnonymousUserMixin):
72
+ @property
73
+ def is_authenticated(self):
74
+ return True
75
+
76
+ def get_id(self):
77
+ return session.get("demo_user_id")
78
+
79
+ login_manager.anonymous_user = SessionDemoUser
80
+
81
+
82
+
83
+ @app.route('/')
84
+ def redirect_to_prefix():
85
+ return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
86
+
87
+ @app.template_filter('format_name')
88
+ def format_name(name):
89
+ name = name.split(".")[-1]
90
+ text = ' '.join(word for word in name.split('_'))
91
+ return text.capitalize()
92
+
93
+ # app.config.setdefault("DEMO_MODE", False)
94
+ return app
@@ -5,7 +5,7 @@ from ivoryos.routes.control.control_file import control_file
5
5
  from ivoryos.routes.control.control_new_device import control_temp
6
6
  from ivoryos.routes.control.utils import post_session_by_instrument, get_session_by_instrument, find_instrument_by_name
7
7
  from ivoryos.utils.global_config import GlobalConfig
8
- from ivoryos.utils.form import create_form_from_module
8
+ from ivoryos.utils.form import create_form_from_module, create_form_from_pseudo
9
9
  from ivoryos.utils.task_runner import TaskRunner
10
10
 
11
11
  global_config = GlobalConfig()
@@ -48,7 +48,10 @@ def deck_controllers(instrument: str = None):
48
48
  forms = None
49
49
  if instrument:
50
50
  inst_object = find_instrument_by_name(instrument)
51
- forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
51
+ if instrument.startswith("blocks"):
52
+ forms = create_form_from_pseudo(pseudo=inst_object, autofill=False, design=False)
53
+ else:
54
+ forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
52
55
  order = get_session_by_instrument('card_order', instrument)
53
56
  hidden_functions = get_session_by_instrument('hidden_functions', instrument)
54
57
  functions = list(forms.keys())
@@ -99,12 +102,11 @@ def deck_controllers(instrument: str = None):
99
102
  function_data["signature"] = str(function_data["signature"])
100
103
  return jsonify(snapshot)
101
104
 
102
- deck_variables = global_config.deck_snapshot.keys()
103
- temp_variables = global_config.defined_variables.keys()
104
105
  return render_template(
105
106
  "controllers.html",
106
- defined_variables=deck_variables,
107
- temp_variables=temp_variables,
107
+ defined_variables=global_config.deck_snapshot.keys(),
108
+ block_variables=global_config.building_blocks.keys(),
109
+ temp_variables=global_config.defined_variables.keys(),
108
110
  instrument=instrument,
109
111
  forms=forms,
110
112
  session=session
@@ -49,6 +49,24 @@
49
49
  </div>
50
50
  </div>
51
51
  {% endif %}
52
+
53
+ {% if block_variables %}
54
+ <div class="mb-4">
55
+ <h6 class="fw-bold text-secondary mb-2" style="letter-spacing: 1px;">Methods</h6>
56
+ <div class="list-group">
57
+ {% for inst in block_variables %}
58
+ <a class="list-group-item list-group-item-action d-flex align-items-center {% if instrument == inst %}active bg-warning text-dark border-0{% else %}bg-light{% endif %}"
59
+ href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
60
+ style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
61
+ <span class="flex-grow-1">{{ inst | format_name }}</span>
62
+ {% if instrument == inst %}
63
+ <span class="ms-auto">&gt;</span>
64
+ {% endif %}
65
+ </a>
66
+ {% endfor %}
67
+ </div>
68
+ </div>
69
+ {% endif %}
52
70
  <!-- Action Buttons -->
53
71
  <div class="mb-4">
54
72
  <a href="{{ url_for('control.file.download_proxy', filetype='proxy') }}" class="btn btn-outline-primary w-100 mb-2">
@@ -13,6 +13,8 @@ def find_instrument_by_name(name: str):
13
13
  if name.startswith("deck"):
14
14
  name = name.replace("deck.", "")
15
15
  return getattr(global_config.deck, name)
16
+ elif name.startswith("blocks"):
17
+ return global_config.building_blocks[name]
16
18
  elif name in global_config.defined_variables:
17
19
  return global_config.defined_variables[name]
18
20
  elif name in globals():
@@ -35,6 +35,9 @@ def _create_forms(instrument, script, autofill, pseudo_deck = None):
35
35
  _object = global_config.defined_variables.get(instrument)
36
36
  functions = utils._inspect_class(_object)
37
37
  forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
38
+ elif instrument.startswith("blocks"):
39
+ forms = create_form_from_pseudo(pseudo=global_config.building_blocks[instrument], autofill=autofill, script=script)
40
+ functions = global_config.building_blocks[instrument]
38
41
  else:
39
42
  if deck:
40
43
  functions = global_config.deck_snapshot.get(instrument, {})
@@ -92,7 +95,7 @@ def experiment_builder():
92
95
 
93
96
  return render_template('experiment_builder.html', off_line=off_line, history=deck_list,
94
97
  script=script, defined_variables=deck_variables, buttons_dict=design_buttons,
95
- local_variables=global_config.defined_variables)
98
+ local_variables=global_config.defined_variables, block_variables=global_config.building_blocks)
96
99
 
97
100
 
98
101
  @design.route("/draft/meta", methods=["PATCH"])
@@ -192,7 +195,8 @@ def update_ui_state():
192
195
  deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
193
196
  deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
194
197
  html = render_template("components/sidebar.html", history=deck_list,
195
- defined_variables=deck_variables, local_variables = global_config.defined_variables)
198
+ defined_variables=deck_variables, local_variables = global_config.defined_variables,
199
+ block_variables=global_config.building_blocks)
196
200
  return jsonify({"html": html})
197
201
  return jsonify({"error": "Invalid request"}), 400
198
202
 
@@ -310,6 +314,7 @@ def methods_handler(instrument: str = ''):
310
314
 
311
315
  success = True
312
316
  msg = ""
317
+ request.form
313
318
  if "hidden_name" in request.form:
314
319
  method_name = request.form.get("hidden_name", None)
315
320
  form = forms.get(method_name) if forms else None
@@ -322,7 +327,7 @@ def methods_handler(instrument: str = ''):
322
327
  primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
323
328
 
324
329
  # todo
325
- print(primitive_arg_types)
330
+ # print(primitive_arg_types)
326
331
 
327
332
  script.eval_list(kwargs, primitive_arg_types)
328
333
  kwargs = script.validate_variables(kwargs)
@@ -422,7 +427,9 @@ def get_operation_sidebar(instrument: str = ''):
422
427
  # edit_action_info = session.get("edit_action")
423
428
  html = render_template("components/sidebar.html", off_line=off_line, history=deck_list,
424
429
  defined_variables=deck_variables,
425
- local_variables=global_config.defined_variables)
430
+ local_variables=global_config.defined_variables,
431
+ block_variables=global_config.building_blocks,
432
+ )
426
433
  return jsonify({"html": html})
427
434
 
428
435
 
@@ -1,6 +1,6 @@
1
1
  {# Action form component #}
2
- <div class="accordion-item design-control" draggable="true">
3
- <h2 class="accordion-header">
2
+ <div class="accordion-item design-control">
3
+ <h2 class="accordion-header" >
4
4
  <button class="accordion-button collapsed draggable-action"
5
5
  type="button" data-bs-toggle="collapse"
6
6
  data-bs-target="#{{name}}" aria-expanded="false"
@@ -39,7 +39,29 @@
39
39
  </ul>
40
40
  </div>
41
41
  </div>
42
-
42
+ {% if block_variables %}
43
+ <div class="accordion-item design-control">
44
+ <h5 class="accordion-header">
45
+ <button class="accordion-button" data-bs-toggle="collapse" data-bs-target="#block" role="button" aria-expanded="false" aria-controls="collapseExample">
46
+ Methods
47
+ </button>
48
+ </h5>
49
+ <div class="accordion-collapse collapse show" id="block">
50
+ <ul class="list-group">
51
+ {% for category in block_variables %}
52
+ <button class="list-group-item list-group-item-action"
53
+ type="button"
54
+ name="device"
55
+ value="{{category}}"
56
+ data-get-url="{{ url_for('design.get_operation_sidebar', instrument=category) }}"
57
+ onclick="updateInstrumentPanel(this)">
58
+ {{ category|format_name }}
59
+ </button>
60
+ {% endfor%}
61
+ </ul>
62
+ </div>
63
+ </div>
64
+ {% endif %}
43
65
  {% if local_variables %}
44
66
  <div class="accordion-item design-control">
45
67
  <h5 class="accordion-header">
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.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
98
+ output_path=dummy_deck_path,
99
+ save=True,
100
+ exclude_names=exclude_names
101
+ )
102
+ global_config.building_blocks = utils.create_block_snapshot()
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,9 +666,28 @@ 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):
657
692
  __tablename__ = 'workflow_runs'
658
693
 
@@ -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
@@ -429,7 +429,7 @@ def create_form_from_action(action: dict, script=None, design=True):
429
429
 
430
430
  def create_all_builtin_forms(script):
431
431
  all_builtin_forms = {}
432
- for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
432
+ for logic_name in ['if', 'while', 'variable', 'wait', 'repeat', 'pause']:
433
433
  # signature = info.get('signature', {})
434
434
  form_class = create_builtin_form(logic_name, script)
435
435
  all_builtin_forms[logic_name] = form_class()
@@ -444,7 +444,8 @@ def create_builtin_form(logic_type, script):
444
444
 
445
445
  placeholder_text = {
446
446
  'wait': 'Enter second',
447
- 'repeat': 'Enter an integer'
447
+ 'repeat': 'Enter an integer',
448
+ 'pause': 'Human Intervention Message'
448
449
  }.get(logic_type, 'Enter statement')
449
450
  description_text = {
450
451
  'variable': 'Your variable can be numbers, boolean (True or False) or text ("text")',
@@ -536,6 +537,7 @@ def _action_button(action: dict, variables: dict):
536
537
  "repeat": "background-color: lightsteelblue",
537
538
  "if": "background-color: salmon",
538
539
  "while": "background-color: salmon",
540
+ "pause": "background-color: goldenrod",
539
541
  }.get(action['instrument'], "")
540
542
 
541
543
  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
@@ -8,12 +8,18 @@ from datetime import datetime
8
8
  from ivoryos.utils import utils, bo_campaign
9
9
  from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db, SingleStep
10
10
  from ivoryos.utils.global_config import GlobalConfig
11
+ from ivoryos.utils.decorators import BUILDING_BLOCKS
11
12
 
12
13
  global_config = GlobalConfig()
13
14
  global deck
14
15
  deck = None
15
16
  # global deck, registered_workflows
16
17
  # deck, registered_workflows = None, None
18
+ class HumanInterventionRequired(Exception):
19
+ pass
20
+
21
+ def pause(reason="Human intervention required"):
22
+ raise HumanInterventionRequired(reason)
17
23
 
18
24
  class ScriptRunner:
19
25
  def __init__(self, globals_dict=None):
@@ -110,9 +116,15 @@ class ScriptRunner:
110
116
  # Parse function body from string
111
117
  temp_connections = global_config.defined_variables
112
118
  # Prepare execution environment
113
- exec_globals = {"deck": deck, "time":time} # Add required global objects
119
+ exec_globals = {"deck": deck, "time":time, "pause": pause} # Add required global objects
114
120
  # exec_globals = {"deck": deck, "time": time, "registered_workflows":registered_workflows} # Add required global objects
115
121
  exec_globals.update(temp_connections)
122
+
123
+ # Inject all block categories
124
+ for category, data in BUILDING_BLOCKS.items():
125
+ for method_name, method in data.items():
126
+ exec_globals[method_name] = method["func"]
127
+
116
128
  exec_locals = {} # Local execution scope
117
129
 
118
130
  # Define function arguments manually in exec_locals
@@ -161,6 +173,13 @@ class ScriptRunner:
161
173
  else:
162
174
  exec(line, exec_globals, exec_locals)
163
175
  step.run_error = False
176
+
177
+ except HumanInterventionRequired as e:
178
+ logger.warning(f"Human intervention required: {e}")
179
+ socketio.emit('human_intervention', {'message': str(e)})
180
+ # Instead of auto-resume, explicitly stay paused until user action
181
+ self.toggle_pause()
182
+
164
183
  except Exception as e:
165
184
  logger.error(f"Error during script execution: {e}")
166
185
  socketio.emit('error', {'message': str(e)})
@@ -230,7 +249,7 @@ class ScriptRunner:
230
249
  with current_app.app_context():
231
250
  run = db.session.get(WorkflowRun, run_id)
232
251
  run.end_time = datetime.now()
233
- run.output_file = filename
252
+ run.data_path = filename
234
253
  run.run_error = error_flag
235
254
  db.session.commit()
236
255
 
@@ -2,6 +2,7 @@ import threading
2
2
  import time
3
3
  from datetime import datetime
4
4
 
5
+ from ivoryos.utils.decorators import BUILDING_BLOCKS
5
6
  from ivoryos.utils.db_models import db, SingleStep
6
7
  from ivoryos.utils.global_config import GlobalConfig
7
8
 
@@ -48,16 +49,20 @@ class TaskRunner:
48
49
  if component.startswith("deck."):
49
50
  component = component.split(".")[1]
50
51
  instrument = getattr(deck, component)
52
+ function_executable = getattr(instrument, method)
53
+ elif component.startswith("blocks."):
54
+ component = component.split(".")[1]
55
+ function_executable = BUILDING_BLOCKS[component][method]["func"]
51
56
  else:
52
57
  temp_connections = global_config.defined_variables
53
58
  instrument = temp_connections.get(component)
54
- function_executable = getattr(instrument, method)
59
+ function_executable = getattr(instrument, method)
55
60
  return function_executable
56
61
 
57
62
  def _run_single_step(self, component, method, kwargs, current_app=None):
58
63
  try:
59
64
  function_executable = self._get_executable(component, deck, method)
60
- method_name = f"{function_executable.__self__.__class__.__name__}.{function_executable.__name__}"
65
+ method_name = f"{component}.{method}"
61
66
  except Exception as e:
62
67
  self.lock.release()
63
68
  return {"status": "error", "msg": e.__str__()}
ivoryos/utils/utils.py CHANGED
@@ -15,7 +15,7 @@ from flask_login import current_user
15
15
  from flask_socketio import SocketIO
16
16
 
17
17
  from ivoryos.utils.db_models import Script
18
-
18
+ from ivoryos.utils.decorators import BUILDING_BLOCKS
19
19
 
20
20
  def get_script_file():
21
21
  """Get script from Flask session and returns the script"""
@@ -151,6 +151,7 @@ def _convert_by_str(args, arg_types):
151
151
  return args
152
152
  except Exception:
153
153
  raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
154
+ return args
154
155
 
155
156
 
156
157
  def _convert_by_class(args, arg_types):
@@ -364,6 +365,24 @@ def create_deck_snapshot(deck, save: bool = False, output_path: str = '', exclud
364
365
  return deck_snapshot
365
366
 
366
367
 
368
+ def create_block_snapshot(save: bool = False, output_path: str = ''):
369
+ block_snapshot = {}
370
+ included = {}
371
+ failed = {}
372
+ for category, data in BUILDING_BLOCKS.items():
373
+ key = f"blocks.{category}"
374
+ block_snapshot[key] = {}
375
+
376
+ for func_name, meta in data.items():
377
+ func = meta["func"]
378
+ block_snapshot[key][func_name] = {
379
+ "signature": meta["signature"],
380
+ "docstring": meta["docstring"],
381
+ "path": f"{func.__module__}.{func.__qualname__}"
382
+ }
383
+ print(block_snapshot)
384
+ return block_snapshot
385
+
367
386
  def load_deck(pkl_name: str):
368
387
  """
369
388
  Loads a pickled deck snapshot from disk on offline mode
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.7"
1
+ __version__ = "1.2.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -1,7 +1,9 @@
1
- ivoryos/__init__.py,sha256=BAA7OPl3h_QdSc4na-7OWTKhaOq1LtHOrJHX3gGITrc,9863
1
+ ivoryos/__init__.py,sha256=eUtNgSskl--l94VUTT1bgiBR8gdMMFQgjHEsHOxdHyI,320
2
+ ivoryos/app.py,sha256=yU3f4mu2LL82NEPvQid5GWdfrIa4F_04Feb0bl-ICV4,3323
2
3
  ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
4
+ ivoryos/server.py,sha256=2ka-xqWpuY9HIqxtA24jU_dIX51DRx1ccejpJnBKgaE,6742
3
5
  ivoryos/socket_handlers.py,sha256=VWVWiIdm4jYAutwGu6R0t1nK5MuMyOCL0xAnFn06jWQ,1302
4
- ivoryos/version.py,sha256=49prCLbE3fFzLfxem5rd2dr1iV4_L-bN0N4J7jxU5yA,22
6
+ ivoryos/version.py,sha256=CfVXm0wwlKPW0khOcwhWw61TpgtZiLijCePsAIOK3aU,22
5
7
  ivoryos/optimizer/ax_optimizer.py,sha256=PoSu8hrDFFpqyhRBnaSMswIUsDfEX6sPWt8NEZ_sobs,7112
6
8
  ivoryos/optimizer/base_optimizer.py,sha256=JTbUharZKn0t8_BDbAFuwZIbT1VOnX1Xuog1pJuU8hY,1992
7
9
  ivoryos/optimizer/baybe_optimizer.py,sha256=EdrrRiYO-IOx610cPXiQhH4qG8knUP0uiZ0YoyaGIU8,7954
@@ -13,11 +15,11 @@ ivoryos/routes/auth/auth.py,sha256=CqoP9cM8BuXVGHGujX7-0sNAOdWILU9amyBrObOD6Ss,3
13
15
  ivoryos/routes/auth/templates/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
14
16
  ivoryos/routes/auth/templates/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
15
17
  ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- ivoryos/routes/control/control.py,sha256=4wqY-68PCJkhdBXzj4sb3BceI0PKViPiTLGz8VP_rU8,6059
18
+ ivoryos/routes/control/control.py,sha256=6LnVF4mGgfLQvzmrSFxaFz9lBtBe4WnXlIouDxtaR2E,6230
17
19
  ivoryos/routes/control/control_file.py,sha256=3fQ9R8EcdqKs_hABn2EqRAB1xC2DHAT_q_pwsMIDDQI,864
18
20
  ivoryos/routes/control/control_new_device.py,sha256=mfJKg5JAOagIpUKbp2b5nRwvd2V3bzT3M0zIhIsEaFM,5456
19
- ivoryos/routes/control/utils.py,sha256=at11wA5HPAZN4BfMaymj1GKEvRTrqi4Wg6cTqUZJDjU,1155
20
- ivoryos/routes/control/templates/controllers.html,sha256=tgtTuns8S2Pf6XKeojinQZ1bz112ieRGLPF5-1cElfE,8030
21
+ ivoryos/routes/control/utils.py,sha256=XlhhqAtOj7n3XfHPDxJ8TvCV2K2I2IixB0CBkl1QeQc,1242
22
+ ivoryos/routes/control/templates/controllers.html,sha256=XoV1HVjB6ho4Ah8HerqqmbiWK0N_HyFKhmqu4E2JXdM,8966
21
23
  ivoryos/routes/control/templates/controllers_new.html,sha256=eVeLABT39DWOIYrwWClw7sAD3lCoAGCznygPgFbQoRc,5945
22
24
  ivoryos/routes/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
25
  ivoryos/routes/data/data.py,sha256=AoqCaIAK0f9hstF1pxlJFeK_J-wKbMfXWGNDUbaBFFk,4218
@@ -25,11 +27,11 @@ ivoryos/routes/data/templates/workflow_database.html,sha256=ofvHcovpwmJXo1SFiSrL
25
27
  ivoryos/routes/data/templates/workflow_view.html,sha256=72xKreX9WhYx-0n0cFf-CL-fJIWXPCIaTi_Aa8Tq3xg,3651
26
28
  ivoryos/routes/data/templates/components/step_card.html,sha256=9lKR4NCgU2v5Nbdv2uaJ-9aKibtiB_2-Y_kyHX6Ka1k,730
27
29
  ivoryos/routes/design/__init__.py,sha256=zS3HXKaw0ALL5n6t_W1rUz5Uj5_tTQ-Y1VMXyzewvR0,113
28
- ivoryos/routes/design/design.py,sha256=BxCSyb9NHlm6SLi7iZITS-g4IyPyBK0ZVAeadPGG-Cw,17763
30
+ ivoryos/routes/design/design.py,sha256=xYDwtCdTcCd282guaIeNvfUFc5UsiypkQVpRvFqRujQ,18246
29
31
  ivoryos/routes/design/design_file.py,sha256=m4yku8fkpLUs4XvLJBqR5V-kyaGKbGB6ZoRxGbjEU5Q,2140
30
32
  ivoryos/routes/design/design_step.py,sha256=l8U3-FuXmap__sYm51AueKdbTaLCFaKjAz-j02b4g-E,5200
31
33
  ivoryos/routes/design/templates/experiment_builder.html,sha256=hh-d2tOc_40gww5WfUYIf8sM3qBaALZnR8Sx7Ja4tpU,1623
32
- ivoryos/routes/design/templates/components/action_form.html,sha256=ktnmXVwe2WFLM6Sg_VbBfDrPXrnongSUxjpYhZGamPY,3058
34
+ ivoryos/routes/design/templates/components/action_form.html,sha256=kXJOrJLbFsMHHWVSuMQHpt1xFrUMnwgzTG8e6Qfn0Cg,3042
33
35
  ivoryos/routes/design/templates/components/actions_panel.html,sha256=jHTR58saTUIZInBdC-vLc1ZTbStLiULeWbupjB4hQzo,977
34
36
  ivoryos/routes/design/templates/components/autofill_toggle.html,sha256=CRVQUHoQT7sOSO5-Vax54ImHdT4G_mEgqR5OQkeUwK8,617
35
37
  ivoryos/routes/design/templates/components/canvas.html,sha256=bKLCJaG1B36Yy9Vsnz4P5qiX4BPdfaGe9JeQQzu9rsI,268
@@ -38,7 +40,7 @@ ivoryos/routes/design/templates/components/canvas_header.html,sha256=7iIzLDGHX7M
38
40
  ivoryos/routes/design/templates/components/canvas_main.html,sha256=9inYO700zRa09lfQI2NY4FJGGeTh-9rvX4ltjj0LK3k,1432
39
41
  ivoryos/routes/design/templates/components/deck_selector.html,sha256=ryTRpljYezo0AzGLCJu_qOMokjjnft3GIxddmNGtBA0,657
40
42
  ivoryos/routes/design/templates/components/edit_action_form.html,sha256=Dz7FnnOK4PYptAHNy9_WFCU1RZTSV61-1lNHHOSRJNs,1876
41
- ivoryos/routes/design/templates/components/instruments_panel.html,sha256=r1jnScVRAknrRPRbAnIVApfnx9f4yavgf9ZlNhNpjW4,3135
43
+ ivoryos/routes/design/templates/components/instruments_panel.html,sha256=tRKd-wOqKjaMJCLuGgRmHtxIgSjklhBkuX8arm5aTCU,4268
42
44
  ivoryos/routes/design/templates/components/modals.html,sha256=6Dl8I8oD4ln7kK8C5e92pFVVH5KDte-vVTL0U_6NSTg,306
43
45
  ivoryos/routes/design/templates/components/python_code_overlay.html,sha256=GUHgsmUWQf0P1Fbg5W0OJC34TXCUIMQVUkS7KDoauyI,1264
44
46
  ivoryos/routes/design/templates/components/sidebar.html,sha256=A6dRo53zIB6QJVrRLJcBZHUNJ3qpYPnR3kWxM8gTkjw,501
@@ -76,25 +78,26 @@ ivoryos/static/js/action_handlers.js,sha256=UJHKFhYRNQRBo0AHLCIxhWxt8OSgYeyLynzP
76
78
  ivoryos/static/js/db_delete.js,sha256=l67fqUaN_FVDaL7v91Hd7LyRbxnqXx9nyjF34-7aewY,561
77
79
  ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
78
80
  ivoryos/static/js/script_metadata.js,sha256=m8VYZ8OGT2oTx1kXMXq60bKQI9WCbJNkzcFDzLvRuGc,1188
79
- ivoryos/static/js/socket_handler.js,sha256=2Iyv_3METjhSlSavs_L9FE3PKY4xDEpfzJpd2FywY9o,5300
81
+ ivoryos/static/js/socket_handler.js,sha256=vrpVyYMsFpHIJjqke5LwVttRI6IJMXSx_D0AMhWRg3k,6906
80
82
  ivoryos/static/js/sortable_card.js,sha256=ifmlGe3yy0U_KzMphV4ClRhK2DLOvkELYMlq1vECuac,807
81
- ivoryos/static/js/sortable_design.js,sha256=QqYyk385JNm6zCgZK_Oa-cJEP1uPtZ_tVz27x4hyx5A,4790
83
+ ivoryos/static/js/sortable_design.js,sha256=ASc9P6_423Mczeg6QH6LVtyxLyWhpxWJP2nEEjR9K1M,5474
82
84
  ivoryos/static/js/ui_state.js,sha256=XYsOcfGlduqLlqHySvPrRrR50CiAsml51duqneigsRY,3368
83
85
  ivoryos/templates/base.html,sha256=cl5w6E8yskbUzdiJFal6fZjnPuFNKEzc7BrrbRd6bMI,8581
84
86
  ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
87
  ivoryos/utils/bo_campaign.py,sha256=Fil-zT7JexL_p9XqyWByjAk42XB1R9XUKN8CdV5bi6c,9714
86
88
  ivoryos/utils/client_proxy.py,sha256=74G3HAuq50iEHkSvlMZFmQaukm613FbRgOdzO_T3dMg,10191
87
- ivoryos/utils/db_models.py,sha256=EN0gNzYgCxKLxgceKEixWi17EKMObz0hLdDnpZ-ur5o,27923
88
- ivoryos/utils/form.py,sha256=A6juCWGtSbaTClgVc8rqufniPRWT7LjeDQs4r0gQs50,22207
89
- ivoryos/utils/global_config.py,sha256=zNO9GYhGn7El3msWoxJIm3S4Mzb3VMh2i5ZEsVtvb2Q,2463
89
+ ivoryos/utils/db_models.py,sha256=aJ9JOmKERXqGrhSTqIbdTGeq16aalDUaOmU3kvZvx0U,29542
90
+ ivoryos/utils/decorators.py,sha256=p1Bdl3dCeaHNv6-cCCUOZMiFu9kRaqqQnkFJUkzPoJE,991
91
+ ivoryos/utils/form.py,sha256=G9wkxCF3aozWBvm865DcCkA6G4dQfNQWKlPejYp5t-U,22311
92
+ ivoryos/utils/global_config.py,sha256=D6oz5dttyaP24jbqnw1sR64moSb-7jJkSpRuufdA_TI,2747
90
93
  ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
91
94
  ivoryos/utils/py_to_json.py,sha256=fyqjaxDHPh-sahgT6IHSn34ktwf6y51_x1qvhbNlH-U,7314
92
- ivoryos/utils/script_runner.py,sha256=MdLMSAeaVXxnbcQfzHJximeJ6W7uOCxqTQhFSpsEieg,17065
95
+ ivoryos/utils/script_runner.py,sha256=gQp37yzlo04h6bxbHloOOim4cLn43S0web7-XN2TQjA,17799
93
96
  ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
94
- ivoryos/utils/task_runner.py,sha256=qgHheE2rnhgRmWUeUQHgdS-Kl-yv-9uA-eM6lD9d0b4,3018
95
- ivoryos/utils/utils.py,sha256=-WiU0_brszB9yDsiQepf_7SzNgPTSpul2RSKDOY3pqo,13921
96
- ivoryos-1.2.7.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
97
- ivoryos-1.2.7.dist-info/METADATA,sha256=EEA3X0SiKMnSGTiZ-Q5_4A6LsofsYSVdKE7L2o4eZhE,7351
98
- ivoryos-1.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
99
- ivoryos-1.2.7.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
100
- ivoryos-1.2.7.dist-info/RECORD,,
97
+ ivoryos/utils/task_runner.py,sha256=bfG6GubdlzgD8rBwzD00aGB5LDFmb9hLFJIOMH8hVv4,3248
98
+ ivoryos/utils/utils.py,sha256=JfDANbhjD6eBJTKeTtEn6B060jd9HACMLxrrQjJzaAI,14589
99
+ ivoryos-1.2.8.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
100
+ ivoryos-1.2.8.dist-info/METADATA,sha256=lw0l8cYCIOBgPxYj3PxoOcAwyLDUL4T6Ij_DrRQsBXc,7351
101
+ ivoryos-1.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
+ ivoryos-1.2.8.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
103
+ ivoryos-1.2.8.dist-info/RECORD,,