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/__init__.py +9 -244
- ivoryos/app.py +131 -0
- ivoryos/routes/api/api.py +2 -1
- ivoryos/routes/control/control.py +8 -6
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +80 -41
- ivoryos/routes/data/templates/components/step_card.html +42 -13
- ivoryos/routes/data/templates/workflow_view.html +334 -113
- ivoryos/routes/design/design.py +11 -4
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/server.py +168 -0
- ivoryos/static/js/socket_handler.js +39 -4
- ivoryos/static/js/sortable_design.js +28 -11
- ivoryos/utils/db_models.py +84 -13
- ivoryos/utils/decorators.py +33 -0
- ivoryos/utils/form.py +8 -4
- ivoryos/utils/global_config.py +10 -0
- ivoryos/utils/script_runner.py +123 -49
- ivoryos/utils/task_runner.py +7 -2
- ivoryos/utils/utils.py +25 -3
- ivoryos/version.py +1 -1
- {ivoryos-1.2.7.dist-info → ivoryos-1.3.0.dist-info}/METADATA +1 -1
- {ivoryos-1.2.7.dist-info → ivoryos-1.3.0.dist-info}/RECORD +28 -25
- {ivoryos-1.2.7.dist-info → ivoryos-1.3.0.dist-info}/WHEEL +0 -0
- {ivoryos-1.2.7.dist-info → ivoryos-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.2.7.dist-info → ivoryos-1.3.0.dist-info}/top_level.txt +0 -0
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');
|
|
42
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
ivoryos/utils/db_models.py
CHANGED
|
@@ -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"{
|
|
614
|
+
single_line = f"{function_call}(**{args_str})"
|
|
599
615
|
elif isinstance(args, str):
|
|
600
|
-
single_line = f"{
|
|
616
|
+
single_line = f"{function_call} = {args}"
|
|
601
617
|
else:
|
|
602
|
-
single_line = f"{
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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 =
|
|
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']:
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -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
|