ivoryos 1.3.3__py3-none-any.whl → 1.3.5a0__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/app.py +2 -1
- ivoryos/routes/control/control.py +2 -2
- ivoryos/routes/design/design.py +4 -1
- ivoryos/server.py +29 -22
- ivoryos/utils/db_models.py +23 -11
- ivoryos/utils/global_config.py +11 -0
- ivoryos/utils/py_to_json.py +19 -4
- ivoryos/utils/script_runner.py +34 -7
- ivoryos/utils/task_runner.py +30 -18
- ivoryos/utils/utils.py +3 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5a0.dist-info}/METADATA +31 -6
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5a0.dist-info}/RECORD +16 -16
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5a0.dist-info}/WHEEL +0 -0
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5a0.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5a0.dist-info}/top_level.txt +0 -0
ivoryos/app.py
CHANGED
|
@@ -41,7 +41,8 @@ def reset_old_schema(engine, db_dir):
|
|
|
41
41
|
old_workflow_run = 'workflow_runs' in tables
|
|
42
42
|
old_workflow_step = 'workflow_steps' in tables
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# v1.3.4 only delete and backup when there is runs but no phases
|
|
45
|
+
if not has_workflow_phase and old_workflow_run:
|
|
45
46
|
print("⚠️ Old workflow database detected! All previous workflows have been reset to support the new schema.")
|
|
46
47
|
# Backup old DB
|
|
47
48
|
db_path = os.path.join(db_dir, "ivoryos.db")
|
|
@@ -23,7 +23,7 @@ control.register_blueprint(control_temp)
|
|
|
23
23
|
@control.route("/", strict_slashes=False, methods=["GET", "POST"])
|
|
24
24
|
@control.route("/<string:instrument>", strict_slashes=False, methods=["GET", "POST"])
|
|
25
25
|
@login_required
|
|
26
|
-
def deck_controllers(instrument: str = None):
|
|
26
|
+
async def deck_controllers(instrument: str = None):
|
|
27
27
|
"""
|
|
28
28
|
.. :quickref: Direct Control; device (instruments) and methods
|
|
29
29
|
|
|
@@ -82,7 +82,7 @@ def deck_controllers(instrument: str = None):
|
|
|
82
82
|
|
|
83
83
|
wait = str(payload.get("hidden_wait", "true")).lower() == "true"
|
|
84
84
|
|
|
85
|
-
output = runner.run_single_step(
|
|
85
|
+
output = await runner.run_single_step(
|
|
86
86
|
component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
|
87
87
|
current_app=current_app._get_current_object()
|
|
88
88
|
)
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -316,6 +316,7 @@ def methods_handler(instrument: str = ''):
|
|
|
316
316
|
msg = ""
|
|
317
317
|
request.form
|
|
318
318
|
if "hidden_name" in request.form:
|
|
319
|
+
deck_snapshot = global_config.deck_snapshot
|
|
319
320
|
method_name = request.form.get("hidden_name", None)
|
|
320
321
|
form = forms.get(method_name) if forms else None
|
|
321
322
|
insert_position = request.form.get("drop_target_id", None)
|
|
@@ -334,7 +335,9 @@ def methods_handler(instrument: str = ''):
|
|
|
334
335
|
action = {"instrument": instrument, "action": function_name,
|
|
335
336
|
"args": kwargs,
|
|
336
337
|
"return": save_data,
|
|
337
|
-
'arg_types': primitive_arg_types
|
|
338
|
+
'arg_types': primitive_arg_types,
|
|
339
|
+
"coroutine": deck_snapshot[instrument][function_name].get("coroutine", False) if deck_snapshot else False,
|
|
340
|
+
}
|
|
338
341
|
script.add_action(action=action, insert_position=insert_position)
|
|
339
342
|
else:
|
|
340
343
|
msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
|
ivoryos/server.py
CHANGED
|
@@ -49,6 +49,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
49
49
|
enable_design: bool = True,
|
|
50
50
|
blueprint_plugins: Union[list, Blueprint] = [],
|
|
51
51
|
exclude_names: list = [],
|
|
52
|
+
notification_handler=None,
|
|
52
53
|
):
|
|
53
54
|
"""
|
|
54
55
|
Start ivoryOS app server.
|
|
@@ -65,6 +66,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
65
66
|
:param enable_design: enable design canvas, database and workflow execution
|
|
66
67
|
:param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
|
|
67
68
|
:param exclude_names: list[str] module names to exclude from parsing
|
|
69
|
+
:param notification_handler: notification handler function
|
|
68
70
|
"""
|
|
69
71
|
app = create_app(config_class=config or get_config()) # Create app instance using factory function
|
|
70
72
|
|
|
@@ -113,11 +115,33 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
113
115
|
output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
|
|
114
116
|
else:
|
|
115
117
|
app.config["ENABLE_LLM"] = False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --- Logger registration ---
|
|
121
|
+
if logger:
|
|
122
|
+
if isinstance(logger, str):
|
|
123
|
+
logger = [logger] # convert single logger to list
|
|
124
|
+
elif not isinstance(logger, list):
|
|
125
|
+
raise TypeError("logger must be a string or a list of strings.")
|
|
126
|
+
|
|
127
|
+
for log_name in logger:
|
|
128
|
+
utils.start_logger(socketio, log_filename=logger_path, logger_name=log_name)
|
|
129
|
+
|
|
130
|
+
# --- Notification handler registration ---
|
|
131
|
+
if notification_handler:
|
|
132
|
+
|
|
133
|
+
# make it a list if a single function is passed
|
|
134
|
+
if callable(notification_handler):
|
|
135
|
+
notification_handler = [notification_handler]
|
|
136
|
+
|
|
137
|
+
if not isinstance(notification_handler, list):
|
|
138
|
+
raise ValueError("notification_handlers must be a callable or a list of callables.")
|
|
139
|
+
|
|
140
|
+
# validate all items are callable
|
|
141
|
+
for handler in notification_handler:
|
|
142
|
+
if not callable(handler):
|
|
143
|
+
raise TypeError(f"Handler {handler} is not callable.")
|
|
144
|
+
global_config.register_notification(handler)
|
|
121
145
|
|
|
122
146
|
# TODO in case Python 3.12 or higher doesn't log URL
|
|
123
147
|
# if sys.version_info >= (3, 12):
|
|
@@ -129,23 +153,6 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
129
153
|
# return app
|
|
130
154
|
|
|
131
155
|
|
|
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
156
|
|
|
150
157
|
def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
|
|
151
158
|
"""
|
ivoryos/utils/db_models.py
CHANGED
|
@@ -434,14 +434,21 @@ class Script(db.Model):
|
|
|
434
434
|
:return: A dict containing script types as keys and lists of function body lines as values.
|
|
435
435
|
"""
|
|
436
436
|
line_collection = {}
|
|
437
|
+
|
|
437
438
|
for stype, func_str in exec_str_collection.items():
|
|
438
439
|
if func_str:
|
|
439
440
|
module = ast.parse(func_str)
|
|
440
|
-
func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
|
|
441
441
|
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
442
|
+
# Find the first function (regular or async)
|
|
443
|
+
func_def = next(
|
|
444
|
+
node for node in module.body
|
|
445
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Extract function body as source lines, skipping 'return' nodes
|
|
449
|
+
line_collection[stype] = [
|
|
450
|
+
ast_unparse(node) for node in func_def.body if not isinstance(node, ast.Return)
|
|
451
|
+
]
|
|
445
452
|
return line_collection
|
|
446
453
|
|
|
447
454
|
def compile(self, script_path=None):
|
|
@@ -459,7 +466,8 @@ class Script(db.Model):
|
|
|
459
466
|
|
|
460
467
|
for i in self.stypes:
|
|
461
468
|
if self.script_dict[i]:
|
|
462
|
-
|
|
469
|
+
is_async = any(a.get("coroutine", False) for a in self.script_dict[i])
|
|
470
|
+
func_str = self._generate_function_header(run_name, i, is_async) + self._generate_function_body(i)
|
|
463
471
|
exec_str_collection[i] = func_str
|
|
464
472
|
if script_path:
|
|
465
473
|
self._write_to_file(script_path, run_name, exec_str_collection)
|
|
@@ -477,7 +485,7 @@ class Script(db.Model):
|
|
|
477
485
|
name += '_'
|
|
478
486
|
return name
|
|
479
487
|
|
|
480
|
-
def _generate_function_header(self, run_name, stype):
|
|
488
|
+
def _generate_function_header(self, run_name, stype, is_async):
|
|
481
489
|
"""
|
|
482
490
|
Generate the function header.
|
|
483
491
|
"""
|
|
@@ -487,7 +495,8 @@ class Script(db.Model):
|
|
|
487
495
|
config_type.items()]
|
|
488
496
|
|
|
489
497
|
script_type = f"_{stype}" if stype != "script" else ""
|
|
490
|
-
|
|
498
|
+
async_str = "async " if is_async else ""
|
|
499
|
+
function_header = f"{async_str}def {run_name}{script_type}("
|
|
491
500
|
|
|
492
501
|
if stype == "script":
|
|
493
502
|
function_header += ", ".join(configure)
|
|
@@ -540,7 +549,8 @@ class Script(db.Model):
|
|
|
540
549
|
# elif instrument == 'registered_workflows':
|
|
541
550
|
# return inspect.getsource(my_function)
|
|
542
551
|
else:
|
|
543
|
-
|
|
552
|
+
is_async = action.get("coroutine", False)
|
|
553
|
+
return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data, is_async)
|
|
544
554
|
|
|
545
555
|
def _process_args(self, args):
|
|
546
556
|
"""
|
|
@@ -600,10 +610,12 @@ class Script(db.Model):
|
|
|
600
610
|
indent_unit -= 1
|
|
601
611
|
return exec_string, indent_unit
|
|
602
612
|
|
|
603
|
-
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data):
|
|
613
|
+
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data, is_async=False):
|
|
604
614
|
"""
|
|
605
615
|
Process actions related to instruments.
|
|
606
616
|
"""
|
|
617
|
+
async_str = "await " if is_async else ""
|
|
618
|
+
|
|
607
619
|
function_call = f"{instrument}.{action}"
|
|
608
620
|
if instrument.startswith("blocks"):
|
|
609
621
|
self.blocks_included = True
|
|
@@ -611,11 +623,11 @@ class Script(db.Model):
|
|
|
611
623
|
|
|
612
624
|
if isinstance(args, dict) and args != {}:
|
|
613
625
|
args_str = self._process_dict_args(args)
|
|
614
|
-
single_line = f"{function_call}(**{args_str})"
|
|
626
|
+
single_line = f"{async_str}{function_call}(**{args_str})"
|
|
615
627
|
elif isinstance(args, str):
|
|
616
628
|
single_line = f"{function_call} = {args}"
|
|
617
629
|
else:
|
|
618
|
-
single_line = f"{function_call}()"
|
|
630
|
+
single_line = f"{async_str}{function_call}()"
|
|
619
631
|
|
|
620
632
|
if save_data:
|
|
621
633
|
save_data += " = "
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -17,6 +17,8 @@ class GlobalConfig:
|
|
|
17
17
|
cls._instance._runner_lock = threading.Lock()
|
|
18
18
|
cls._instance._runner_status = None
|
|
19
19
|
cls._instance._optimizers = {}
|
|
20
|
+
cls._instance._notification_handlers = []
|
|
21
|
+
|
|
20
22
|
return cls._instance
|
|
21
23
|
|
|
22
24
|
@property
|
|
@@ -28,6 +30,15 @@ class GlobalConfig:
|
|
|
28
30
|
if self._deck is None:
|
|
29
31
|
self._deck = value
|
|
30
32
|
|
|
33
|
+
def register_notification(self, handler):
|
|
34
|
+
if not callable(handler):
|
|
35
|
+
raise ValueError("Handler must be callable")
|
|
36
|
+
self._notification_handlers.append(handler)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def notification_handlers(self):
|
|
40
|
+
return self._notification_handlers
|
|
41
|
+
|
|
31
42
|
@property
|
|
32
43
|
def building_blocks(self):
|
|
33
44
|
return self._building_blocks
|
ivoryos/utils/py_to_json.py
CHANGED
|
@@ -55,6 +55,10 @@ def convert_to_cards(source_code: str):
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
class CardVisitor(ast.NodeVisitor):
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.defined_types = {} # <-- always exists
|
|
60
|
+
|
|
61
|
+
|
|
58
62
|
def visit_FunctionDef(self, node):
|
|
59
63
|
self.defined_types = {
|
|
60
64
|
arg.arg: ast.unparse(arg.annotation) if arg.annotation else "float"
|
|
@@ -142,14 +146,20 @@ def convert_to_cards(source_code: str):
|
|
|
142
146
|
"return": "",
|
|
143
147
|
"uuid": generate_uuid()
|
|
144
148
|
})
|
|
149
|
+
elif isinstance(node.value, ast.Await):
|
|
150
|
+
self.handle_call(node.value.value, ret_var=node.targets[0].id, awaited=True)
|
|
151
|
+
|
|
145
152
|
elif isinstance(node.value, ast.Call):
|
|
146
153
|
self.handle_call(node.value, ret_var=node.targets[0].id)
|
|
147
154
|
|
|
148
155
|
def visit_Expr(self, node):
|
|
149
|
-
if isinstance(node.value, ast.
|
|
156
|
+
if isinstance(node.value, ast.Await):
|
|
157
|
+
# node.value is ast.Await
|
|
158
|
+
self.handle_call(node.value.value, awaited=True)
|
|
159
|
+
elif isinstance(node.value, ast.Call):
|
|
150
160
|
self.handle_call(node.value)
|
|
151
161
|
|
|
152
|
-
def handle_call(self, node, ret_var=""):
|
|
162
|
+
def handle_call(self, node, ret_var="", awaited=False):
|
|
153
163
|
func_parts = []
|
|
154
164
|
f = node.func
|
|
155
165
|
while isinstance(f, ast.Attribute):
|
|
@@ -229,7 +239,7 @@ def convert_to_cards(source_code: str):
|
|
|
229
239
|
else infer_type(value)
|
|
230
240
|
)
|
|
231
241
|
|
|
232
|
-
|
|
242
|
+
card = {
|
|
233
243
|
"action": action,
|
|
234
244
|
"arg_types": arg_types,
|
|
235
245
|
"args": args,
|
|
@@ -237,7 +247,12 @@ def convert_to_cards(source_code: str):
|
|
|
237
247
|
"instrument": instrument,
|
|
238
248
|
"return": ret_var,
|
|
239
249
|
"uuid": generate_uuid()
|
|
240
|
-
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if awaited:
|
|
253
|
+
card["coroutine"] = True # mark as coroutine if awaited
|
|
254
|
+
|
|
255
|
+
add_card(card)
|
|
241
256
|
|
|
242
257
|
CardVisitor().visit(tree)
|
|
243
258
|
return cards
|
ivoryos/utils/script_runner.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ast
|
|
2
|
+
import asyncio
|
|
2
3
|
import os
|
|
3
4
|
import csv
|
|
4
5
|
import threading
|
|
@@ -19,6 +20,14 @@ class HumanInterventionRequired(Exception):
|
|
|
19
20
|
pass
|
|
20
21
|
|
|
21
22
|
def pause(reason="Human intervention required"):
|
|
23
|
+
handlers = global_config.notification_handlers
|
|
24
|
+
if handlers:
|
|
25
|
+
for handler in handlers:
|
|
26
|
+
try:
|
|
27
|
+
handler(reason)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print(f"[notify] handler {handler} failed: {e}")
|
|
30
|
+
# raise error to pause workflow in gui
|
|
22
31
|
raise HumanInterventionRequired(reason)
|
|
23
32
|
|
|
24
33
|
class ScriptRunner:
|
|
@@ -161,7 +170,7 @@ class ScriptRunner:
|
|
|
161
170
|
start_time=datetime.now(),
|
|
162
171
|
)
|
|
163
172
|
db.session.add(step)
|
|
164
|
-
db.session.
|
|
173
|
+
db.session.flush()
|
|
165
174
|
|
|
166
175
|
logger.info(f"Executing: {line}")
|
|
167
176
|
socketio.emit('execution', {'section': f"{section_name}-{index}"})
|
|
@@ -174,7 +183,25 @@ class ScriptRunner:
|
|
|
174
183
|
duration = float(duration_str)
|
|
175
184
|
self.safe_sleep(duration)
|
|
176
185
|
else:
|
|
177
|
-
|
|
186
|
+
if "await " in line:
|
|
187
|
+
async_code = f"async def __async_exec_wrapper():\n"
|
|
188
|
+
# indent all code lines by 4 spaces
|
|
189
|
+
async_code += "\n".join(" " + line for line in line.splitlines())
|
|
190
|
+
async_code += f"\n return locals()"
|
|
191
|
+
exec(async_code, exec_globals, exec_locals)
|
|
192
|
+
func = exec_locals.get("__async_exec_wrapper") or exec_globals.get("__async_exec_wrapper")
|
|
193
|
+
# Capture the return value from asyncio.run
|
|
194
|
+
result_locals = asyncio.run(func())
|
|
195
|
+
|
|
196
|
+
# Update exec_locals with the returned locals
|
|
197
|
+
exec_locals.update(result_locals)
|
|
198
|
+
exec_locals.pop("__async_exec_wrapper", None)
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
print("just exec synchronously")
|
|
202
|
+
exec(line, exec_globals, exec_locals)
|
|
203
|
+
# return locals_dict
|
|
204
|
+
# exec(line, exec_globals, exec_locals)
|
|
178
205
|
# step.run_error = False
|
|
179
206
|
|
|
180
207
|
except HumanInterventionRequired as e:
|
|
@@ -219,7 +246,7 @@ class ScriptRunner:
|
|
|
219
246
|
repeat_mode=repeat_mode
|
|
220
247
|
)
|
|
221
248
|
db.session.add(run)
|
|
222
|
-
db.session.
|
|
249
|
+
db.session.flush()
|
|
223
250
|
run_id = run.id # Save the ID
|
|
224
251
|
try:
|
|
225
252
|
|
|
@@ -241,7 +268,7 @@ class ScriptRunner:
|
|
|
241
268
|
self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
|
|
242
269
|
# Reset the running flag when done
|
|
243
270
|
# Save results if necessary
|
|
244
|
-
if not script.python_script and
|
|
271
|
+
if not script.python_script and return_list:
|
|
245
272
|
filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
|
246
273
|
self._emit_progress(socketio, 100)
|
|
247
274
|
|
|
@@ -277,7 +304,7 @@ class ScriptRunner:
|
|
|
277
304
|
start_time=datetime.now()
|
|
278
305
|
)
|
|
279
306
|
db.session.add(phase)
|
|
280
|
-
db.session.
|
|
307
|
+
db.session.flush()
|
|
281
308
|
phase_id = phase.id
|
|
282
309
|
|
|
283
310
|
step_outputs = self.exec_steps(script, section_name, logger, socketio, phase_id=phase_id)
|
|
@@ -317,7 +344,7 @@ class ScriptRunner:
|
|
|
317
344
|
start_time=datetime.now()
|
|
318
345
|
)
|
|
319
346
|
db.session.add(phase)
|
|
320
|
-
db.session.
|
|
347
|
+
db.session.flush()
|
|
321
348
|
|
|
322
349
|
phase_id = phase.id
|
|
323
350
|
output = self.exec_steps(script, "script", logger, socketio, phase_id, **kwargs)
|
|
@@ -371,7 +398,7 @@ class ScriptRunner:
|
|
|
371
398
|
start_time=datetime.now()
|
|
372
399
|
)
|
|
373
400
|
db.session.add(phase)
|
|
374
|
-
db.session.
|
|
401
|
+
db.session.flush()
|
|
375
402
|
phase_id = phase.id
|
|
376
403
|
|
|
377
404
|
logger.info(f'Executing {run_name} experiment: {i_progress + 1}/{int(repeat_count)}')
|
ivoryos/utils/task_runner.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import asyncio
|
|
1
3
|
import threading
|
|
2
4
|
import time
|
|
3
5
|
from datetime import datetime
|
|
@@ -19,8 +21,7 @@ class TaskRunner:
|
|
|
19
21
|
self.globals_dict = globals_dict
|
|
20
22
|
self.lock = global_config.runner_lock
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
|
|
24
|
+
async def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
|
|
24
25
|
global deck
|
|
25
26
|
if deck is None:
|
|
26
27
|
deck = global_config.deck
|
|
@@ -29,18 +30,18 @@ class TaskRunner:
|
|
|
29
30
|
if not self.lock.acquire(blocking=False):
|
|
30
31
|
current_status = global_config.runner_status
|
|
31
32
|
current_status["status"] = "busy"
|
|
33
|
+
current_status["output"] = "busy"
|
|
32
34
|
return current_status
|
|
33
35
|
|
|
34
|
-
|
|
35
36
|
if wait:
|
|
36
|
-
output = self._run_single_step(component, method, kwargs, current_app)
|
|
37
|
+
output = await self._run_single_step(component, method, kwargs, current_app)
|
|
37
38
|
else:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
# Create background task properly
|
|
40
|
+
async def background_runner():
|
|
41
|
+
await self._run_single_step(component, method, kwargs, current_app)
|
|
42
|
+
|
|
43
|
+
asyncio.create_task(background_runner())
|
|
44
|
+
await asyncio.sleep(0.1) # Change time.sleep to await asyncio.sleep
|
|
44
45
|
output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
|
|
45
46
|
|
|
46
47
|
return output
|
|
@@ -59,22 +60,32 @@ class TaskRunner:
|
|
|
59
60
|
function_executable = getattr(instrument, method)
|
|
60
61
|
return function_executable
|
|
61
62
|
|
|
62
|
-
def _run_single_step(self, component, method, kwargs, current_app=None):
|
|
63
|
+
async def _run_single_step(self, component, method, kwargs, current_app=None):
|
|
63
64
|
try:
|
|
64
65
|
function_executable = self._get_executable(component, deck, method)
|
|
65
66
|
method_name = f"{component}.{method}"
|
|
66
67
|
except Exception as e:
|
|
67
68
|
self.lock.release()
|
|
68
|
-
return {"status": "error", "msg": e
|
|
69
|
+
return {"status": "error", "msg": str(e)}
|
|
69
70
|
|
|
70
|
-
# with
|
|
71
|
+
# Flask context is NOT async → just use normal "with"
|
|
71
72
|
with current_app.app_context():
|
|
72
|
-
step = SingleStep(
|
|
73
|
+
step = SingleStep(
|
|
74
|
+
method_name=method_name,
|
|
75
|
+
kwargs=kwargs,
|
|
76
|
+
run_error=None,
|
|
77
|
+
start_time=datetime.now()
|
|
78
|
+
)
|
|
73
79
|
db.session.add(step)
|
|
74
|
-
db.session.
|
|
75
|
-
global_config.runner_status = {"id":step.id, "type": "task"}
|
|
80
|
+
db.session.flush()
|
|
81
|
+
global_config.runner_status = {"id": step.id, "type": "task"}
|
|
82
|
+
|
|
76
83
|
try:
|
|
77
|
-
|
|
84
|
+
if inspect.iscoroutinefunction(function_executable):
|
|
85
|
+
output = await function_executable(**kwargs)
|
|
86
|
+
else:
|
|
87
|
+
output = function_executable(**kwargs)
|
|
88
|
+
|
|
78
89
|
step.output = output
|
|
79
90
|
step.end_time = datetime.now()
|
|
80
91
|
success = True
|
|
@@ -86,4 +97,5 @@ class TaskRunner:
|
|
|
86
97
|
finally:
|
|
87
98
|
db.session.commit()
|
|
88
99
|
self.lock.release()
|
|
89
|
-
|
|
100
|
+
|
|
101
|
+
return dict(success=success, output=output)
|
ivoryos/utils/utils.py
CHANGED
|
@@ -105,7 +105,8 @@ def _inspect_class(class_object=None, debug=False):
|
|
|
105
105
|
try:
|
|
106
106
|
annotation = inspect.signature(method)
|
|
107
107
|
docstring = inspect.getdoc(method)
|
|
108
|
-
|
|
108
|
+
coroutine = inspect.iscoroutinefunction(method)
|
|
109
|
+
functions[function] = dict(signature=annotation, docstring=docstring, coroutine=coroutine,)
|
|
109
110
|
|
|
110
111
|
except Exception:
|
|
111
112
|
pass
|
|
@@ -141,6 +142,7 @@ def _get_type_from_parameters(arg, parameters):
|
|
|
141
142
|
def _convert_by_str(args, arg_types):
|
|
142
143
|
"""
|
|
143
144
|
Converts a value to type through eval(f'{type}("{args}")')
|
|
145
|
+
v1.3.4 TODO try str lastly, otherwise it's always converted to str
|
|
144
146
|
"""
|
|
145
147
|
if type(arg_types) is not list:
|
|
146
148
|
arg_types = [arg_types]
|
ivoryos/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.3.
|
|
1
|
+
__version__ = "1.3.5a0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivoryos
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5a0
|
|
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
|
|
@@ -28,7 +28,8 @@ Dynamic: license-file
|
|
|
28
28
|
[](https://youtu.be/dFfJv9I2-1g)
|
|
29
29
|
[](https://youtu.be/flr5ydiE96s)
|
|
30
30
|
[](https://www.nature.com/articles/s41467-025-60514-w)
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
[//]: # ([](https://discord.gg/AX5P9EdGVX))
|
|
32
33
|
|
|
33
34
|

|
|
34
35
|
# ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
|
|
@@ -101,11 +102,14 @@ pip install -e .
|
|
|
101
102
|
## Quick start
|
|
102
103
|
In your SDL script,
|
|
103
104
|
```python
|
|
105
|
+
my_robot = Robot()
|
|
106
|
+
|
|
104
107
|
import ivoryos
|
|
105
108
|
|
|
106
109
|
ivoryos.run(__name__)
|
|
107
110
|
```
|
|
108
|
-
|
|
111
|
+
You can now access the web UI at http://127.0.0.1:8000,
|
|
112
|
+
create an account, login, and start designing workflows!
|
|
109
113
|
|
|
110
114
|
----
|
|
111
115
|
## Features
|
|
@@ -127,6 +131,28 @@ Add single or multiple loggers:
|
|
|
127
131
|
ivoryos.run(__name__, logger="logger name")
|
|
128
132
|
ivoryos.run(__name__, logger=["logger 1", "logger 2"])
|
|
129
133
|
```
|
|
134
|
+
### Human-in-the-loop
|
|
135
|
+
Add single or multiple notification handlers for `pause` feature in flow control:
|
|
136
|
+
```python
|
|
137
|
+
|
|
138
|
+
def slack_bot(msg: str = "Hi"):
|
|
139
|
+
"""
|
|
140
|
+
a function that can be used as a notification handler function("msg")
|
|
141
|
+
:param msg: message to send
|
|
142
|
+
"""
|
|
143
|
+
from slack_sdk import WebClient
|
|
144
|
+
|
|
145
|
+
slack_token = "your slack token"
|
|
146
|
+
client = WebClient(token=slack_token)
|
|
147
|
+
|
|
148
|
+
my_user_id = "your user id" # replace with your actual Slack user ID
|
|
149
|
+
|
|
150
|
+
client.chat_postMessage(channel=my_user_id, text=msg)
|
|
151
|
+
|
|
152
|
+
import ivoryos
|
|
153
|
+
ivoryos.run(__name__, notification_handler=slack_bot)
|
|
154
|
+
```
|
|
155
|
+
|
|
130
156
|
### Directory Structure
|
|
131
157
|
|
|
132
158
|
Created automatically on first run:
|
|
@@ -152,11 +178,10 @@ ivoryos.run(__name__)
|
|
|
152
178
|
|
|
153
179
|
## Roadmap
|
|
154
180
|
|
|
155
|
-
- [x] Allow plugin pages ✅
|
|
156
|
-
- [x] pause, resume, abort current and pending workflows ✅
|
|
157
181
|
- [ ] dropdown input
|
|
158
182
|
- [ ] snapshot version control
|
|
159
183
|
- [ ] optimizer-agnostic
|
|
184
|
+
- [ ] prefect compatibility
|
|
160
185
|
- [ ] check batch-config file compatibility
|
|
161
186
|
|
|
162
187
|
---
|
|
@@ -198,4 +223,4 @@ For an additional perspective related to the development of the tool, please see
|
|
|
198
223
|
```
|
|
199
224
|
---
|
|
200
225
|
## Acknowledgements
|
|
201
|
-
Authors acknowledge Telescope Innovations Corp., Hein Lab members for their valuable suggestions and contributions.
|
|
226
|
+
Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
ivoryos/__init__.py,sha256=Or4kPfDSQA5WYrQdnBlWl_dA4wwjhEqNkuE5XPX3bqI,358
|
|
2
|
-
ivoryos/app.py,sha256=
|
|
2
|
+
ivoryos/app.py,sha256=G6kzEOVzCduj7Fc2r1rMbMFHgDzZQV0lC20Oxps7RSM,4839
|
|
3
3
|
ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
|
|
4
|
-
ivoryos/server.py,sha256=
|
|
4
|
+
ivoryos/server.py,sha256=5tXrgG16lm6Fl-Q-ZHxiGQQoyZQFxRhdDf6idNYqhNg,6985
|
|
5
5
|
ivoryos/socket_handlers.py,sha256=VWVWiIdm4jYAutwGu6R0t1nK5MuMyOCL0xAnFn06jWQ,1302
|
|
6
|
-
ivoryos/version.py,sha256=
|
|
6
|
+
ivoryos/version.py,sha256=6_TWXwv9gjZoVnzn72CakHZpbe6qkvg_TQ50w9uFBsk,24
|
|
7
7
|
ivoryos/optimizer/ax_optimizer.py,sha256=PoSu8hrDFFpqyhRBnaSMswIUsDfEX6sPWt8NEZ_sobs,7112
|
|
8
8
|
ivoryos/optimizer/base_optimizer.py,sha256=JTbUharZKn0t8_BDbAFuwZIbT1VOnX1Xuog1pJuU8hY,1992
|
|
9
9
|
ivoryos/optimizer/baybe_optimizer.py,sha256=EdrrRiYO-IOx610cPXiQhH4qG8knUP0uiZ0YoyaGIU8,7954
|
|
@@ -15,7 +15,7 @@ ivoryos/routes/auth/auth.py,sha256=D-sPEaUsc8vefly36h74rgU-ILulf7hiK6xc5sBEl58,3
|
|
|
15
15
|
ivoryos/routes/auth/templates/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
|
|
16
16
|
ivoryos/routes/auth/templates/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
|
|
17
17
|
ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
ivoryos/routes/control/control.py,sha256=
|
|
18
|
+
ivoryos/routes/control/control.py,sha256=0g23PoPPoTeMD7Szm3UAW10sEMLpSD-iTfASAZE4iIE,6442
|
|
19
19
|
ivoryos/routes/control/control_file.py,sha256=3fQ9R8EcdqKs_hABn2EqRAB1xC2DHAT_q_pwsMIDDQI,864
|
|
20
20
|
ivoryos/routes/control/control_new_device.py,sha256=mfJKg5JAOagIpUKbp2b5nRwvd2V3bzT3M0zIhIsEaFM,5456
|
|
21
21
|
ivoryos/routes/control/utils.py,sha256=XlhhqAtOj7n3XfHPDxJ8TvCV2K2I2IixB0CBkl1QeQc,1242
|
|
@@ -27,7 +27,7 @@ ivoryos/routes/data/templates/workflow_database.html,sha256=ofvHcovpwmJXo1SFiSrL
|
|
|
27
27
|
ivoryos/routes/data/templates/workflow_view.html,sha256=Ti17kzlPlYTmzx5MkdsPlXJ1_k6QgMYQBM6FHjG50go,12491
|
|
28
28
|
ivoryos/routes/data/templates/components/step_card.html,sha256=XWsr7qxAY76RCuQHETubWjWBlPgs2HkviH4ju6qfBKo,1923
|
|
29
29
|
ivoryos/routes/design/__init__.py,sha256=zS3HXKaw0ALL5n6t_W1rUz5Uj5_tTQ-Y1VMXyzewvR0,113
|
|
30
|
-
ivoryos/routes/design/design.py,sha256=
|
|
30
|
+
ivoryos/routes/design/design.py,sha256=AzipVe9paK5gR_AHZggAoiNTm76rO7i5kTeLs04I8vA,18457
|
|
31
31
|
ivoryos/routes/design/design_file.py,sha256=MVIc5uGSaGxZhs86hfPjX2n0iy1OcXeLq7b9Ucdg4VQ,2115
|
|
32
32
|
ivoryos/routes/design/design_step.py,sha256=zW6kOhX-zJbtX1x5NaVPm79HZjKBzAWevmqJ2Y3qqpg,5218
|
|
33
33
|
ivoryos/routes/design/templates/experiment_builder.html,sha256=hh-d2tOc_40gww5WfUYIf8sM3qBaALZnR8Sx7Ja4tpU,1623
|
|
@@ -86,18 +86,18 @@ ivoryos/templates/base.html,sha256=cl5w6E8yskbUzdiJFal6fZjnPuFNKEzc7BrrbRd6bMI,8
|
|
|
86
86
|
ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
87
|
ivoryos/utils/bo_campaign.py,sha256=Fil-zT7JexL_p9XqyWByjAk42XB1R9XUKN8CdV5bi6c,9714
|
|
88
88
|
ivoryos/utils/client_proxy.py,sha256=74G3HAuq50iEHkSvlMZFmQaukm613FbRgOdzO_T3dMg,10191
|
|
89
|
-
ivoryos/utils/db_models.py,sha256=
|
|
89
|
+
ivoryos/utils/db_models.py,sha256=i1fLiWFnese2xqAfWipN9eUDHeuy6eIYE9_cY6YZ6NU,31553
|
|
90
90
|
ivoryos/utils/decorators.py,sha256=p1Bdl3dCeaHNv6-cCCUOZMiFu9kRaqqQnkFJUkzPoJE,991
|
|
91
91
|
ivoryos/utils/form.py,sha256=Ej9tx06KZZ5fPQm1ho1byotNocF3u24aatc2ZyI0rK4,22301
|
|
92
|
-
ivoryos/utils/global_config.py,sha256=
|
|
92
|
+
ivoryos/utils/global_config.py,sha256=leYoEXvAS0AH4xQpYsqu4HI9CJ9-wiLM-pIh_bEG4Ak,3087
|
|
93
93
|
ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
|
|
94
|
-
ivoryos/utils/py_to_json.py,sha256=
|
|
95
|
-
ivoryos/utils/script_runner.py,sha256=
|
|
94
|
+
ivoryos/utils/py_to_json.py,sha256=ZtejHgwdEAUCVVMYeVNR8G7ceLINue294q6WpiJ6jn0,9734
|
|
95
|
+
ivoryos/utils/script_runner.py,sha256=8mbgPod3j-qOUPnDdbzFkKOn3ROyt-WedkqC01DXON8,20701
|
|
96
96
|
ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
|
|
97
|
-
ivoryos/utils/task_runner.py,sha256=
|
|
98
|
-
ivoryos/utils/utils.py,sha256=
|
|
99
|
-
ivoryos-1.3.
|
|
100
|
-
ivoryos-1.3.
|
|
101
|
-
ivoryos-1.3.
|
|
102
|
-
ivoryos-1.3.
|
|
103
|
-
ivoryos-1.3.
|
|
97
|
+
ivoryos/utils/task_runner.py,sha256=xiMzK8gQ0mHsg0A1Ah8fmXe3azpaJh4hJiQJLHA11ZQ,3682
|
|
98
|
+
ivoryos/utils/utils.py,sha256=dIc4BO55eS3lCA0nhbIncS5d7sLaKZy5hJS1I_Sm45o,14949
|
|
99
|
+
ivoryos-1.3.5a0.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
|
|
100
|
+
ivoryos-1.3.5a0.dist-info/METADATA,sha256=ss3QiOlGF-riNBwIyI8jQumEfxfkIS9Z7134bJABl00,8023
|
|
101
|
+
ivoryos-1.3.5a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
102
|
+
ivoryos-1.3.5a0.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
|
|
103
|
+
ivoryos-1.3.5a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|