ivoryos 0.1.21__py3-none-any.whl → 0.1.23__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 +36 -10
- ivoryos/routes/control/control.py +2 -2
- ivoryos/routes/control/templates/control/controllers_home.html +6 -1
- ivoryos/routes/database/database.py +59 -2
- ivoryos/routes/database/templates/database/experiment_database.html +3 -3
- ivoryos/routes/database/templates/database/experiment_step_view.html +130 -0
- ivoryos/routes/database/templates/database/step_card.html +7 -0
- ivoryos/routes/database/templates/database/workflow_run_database.html +81 -0
- ivoryos/routes/design/design.py +23 -2
- ivoryos/routes/design/templates/design/experiment_builder.html +57 -10
- ivoryos/routes/design/templates/design/experiment_run.html +162 -83
- ivoryos/routes/main/templates/main/home.html +80 -47
- ivoryos/static/js/socket_handler.js +5 -0
- ivoryos/templates/base.html +6 -3
- ivoryos/utils/client_proxy.py +57 -0
- ivoryos/utils/db_models.py +43 -1
- ivoryos/utils/form.py +52 -7
- ivoryos/utils/llm_agent.py +1 -1
- ivoryos/utils/script_runner.py +111 -44
- ivoryos/utils/utils.py +23 -0
- ivoryos/version.py +1 -1
- {ivoryos-0.1.21.dist-info → ivoryos-0.1.23.dist-info}/METADATA +7 -3
- {ivoryos-0.1.21.dist-info → ivoryos-0.1.23.dist-info}/RECORD +26 -22
- {ivoryos-0.1.21.dist-info → ivoryos-0.1.23.dist-info}/LICENSE +0 -0
- {ivoryos-0.1.21.dist-info → ivoryos-0.1.23.dist-info}/WHEEL +0 -0
- {ivoryos-0.1.21.dist-info → ivoryos-0.1.23.dist-info}/top_level.txt +0 -0
ivoryos/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import sys
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
|
-
from flask import Flask, redirect, url_for, g
|
|
5
|
+
from flask import Flask, redirect, url_for, g, Blueprint
|
|
6
6
|
|
|
7
7
|
from ivoryos.config import Config, get_config
|
|
8
8
|
from ivoryos.routes.auth.auth import auth, login_manager
|
|
@@ -18,6 +18,16 @@ from ivoryos.utils.script_runner import ScriptRunner
|
|
|
18
18
|
from ivoryos.version import __version__ as ivoryos_version
|
|
19
19
|
from importlib.metadata import entry_points
|
|
20
20
|
global_config = GlobalConfig()
|
|
21
|
+
from sqlalchemy import event
|
|
22
|
+
from sqlalchemy.engine import Engine
|
|
23
|
+
import sqlite3
|
|
24
|
+
|
|
25
|
+
@event.listens_for(Engine, "connect")
|
|
26
|
+
def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
|
|
27
|
+
if isinstance(dbapi_connection, sqlite3.Connection):
|
|
28
|
+
cursor = dbapi_connection.cursor()
|
|
29
|
+
cursor.execute("PRAGMA foreign_keys=ON")
|
|
30
|
+
cursor.close()
|
|
21
31
|
|
|
22
32
|
url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
|
|
23
33
|
app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
|
|
@@ -29,8 +39,9 @@ app.register_blueprint(database, url_prefix=url_prefix)
|
|
|
29
39
|
|
|
30
40
|
|
|
31
41
|
def create_app(config_class=None):
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
"""
|
|
43
|
+
create app, init database
|
|
44
|
+
"""
|
|
34
45
|
app.config.from_object(config_class or 'config.get_config()')
|
|
35
46
|
|
|
36
47
|
# Initialize extensions
|
|
@@ -70,7 +81,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
70
81
|
config: Config = None,
|
|
71
82
|
logger: Union[str, list] = None,
|
|
72
83
|
logger_output_name: str = None,
|
|
73
|
-
enable_design=True
|
|
84
|
+
enable_design=True,
|
|
85
|
+
blueprint_plugins=None,
|
|
74
86
|
):
|
|
75
87
|
"""
|
|
76
88
|
Start ivoryOS app server.
|
|
@@ -85,11 +97,14 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
85
97
|
:param logger: logger name of list of logger names, defaults to None
|
|
86
98
|
:param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
|
|
87
99
|
:param enable_design: enable design canvas, database and workflow execution
|
|
88
|
-
:param stream_address:
|
|
89
100
|
"""
|
|
90
101
|
app = create_app(config_class=config or get_config()) # Create app instance using factory function
|
|
91
102
|
|
|
92
|
-
plugins =
|
|
103
|
+
plugins = load_installed_plugins(app, socketio)
|
|
104
|
+
|
|
105
|
+
if blueprint_plugins:
|
|
106
|
+
config_plugins = load_plugins(blueprint_plugins, app, socketio)
|
|
107
|
+
plugins.extend(config_plugins)
|
|
93
108
|
|
|
94
109
|
def inject_nav_config():
|
|
95
110
|
"""Make NAV_CONFIG available globally to all templates."""
|
|
@@ -110,10 +125,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
110
125
|
app.config["MODULE"] = module
|
|
111
126
|
app.config["OFF_LINE"] = False
|
|
112
127
|
global_config.deck = sys.modules[module]
|
|
113
|
-
# global_config.heinsight = HeinsightAPI("http://127.0.0.1:8080")
|
|
114
128
|
global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
|
|
115
129
|
output_path=app.config["DUMMY_DECK"], save=True)
|
|
116
|
-
# global_config.runner = ScriptRunner(globals())
|
|
117
130
|
else:
|
|
118
131
|
app.config["OFF_LINE"] = True
|
|
119
132
|
if model:
|
|
@@ -135,7 +148,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
135
148
|
# return app
|
|
136
149
|
|
|
137
150
|
|
|
138
|
-
def
|
|
151
|
+
def load_installed_plugins(app, socketio):
|
|
139
152
|
"""
|
|
140
153
|
Dynamically load installed plugins and attach Flask-SocketIO.
|
|
141
154
|
"""
|
|
@@ -152,4 +165,17 @@ def load_plugins(app, socketio):
|
|
|
152
165
|
|
|
153
166
|
return plugin_names
|
|
154
167
|
|
|
155
|
-
|
|
168
|
+
def load_plugins(blueprints:list[Blueprint], app, socketio):
|
|
169
|
+
"""
|
|
170
|
+
Dynamically load installed plugins and attach Flask-SocketIO.
|
|
171
|
+
"""
|
|
172
|
+
plugin_names = []
|
|
173
|
+
if not isinstance(blueprints, list):
|
|
174
|
+
blueprints = [blueprints]
|
|
175
|
+
for blueprint in blueprints:
|
|
176
|
+
# If the plugin has an `init_socketio()` function, pass socketio
|
|
177
|
+
if hasattr(blueprint, 'init_socketio'):
|
|
178
|
+
blueprint.init_socketio(socketio)
|
|
179
|
+
plugin_names.append(blueprint.name)
|
|
180
|
+
app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
|
|
181
|
+
return plugin_names
|
|
@@ -151,7 +151,7 @@ def controllers(instrument: str):
|
|
|
151
151
|
return render_template('controllers.html', instrument=instrument, forms=forms, format_name=format_name)
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
@control.route("/backend_control/<instrument>", methods=['
|
|
154
|
+
@control.route("/backend_control/<instrument>", methods=['POST'])
|
|
155
155
|
def backend_control(instrument: str=None):
|
|
156
156
|
"""
|
|
157
157
|
.. :quickref: Backend Control; backend control
|
|
@@ -187,7 +187,7 @@ def backend_control(instrument: str=None):
|
|
|
187
187
|
return json_output, 400
|
|
188
188
|
else:
|
|
189
189
|
return "instrument not exist", 400
|
|
190
|
-
|
|
190
|
+
return json_output, 200
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
@control.route("/backend_control", methods=['GET'])
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{# {% if not deck %}#}
|
|
10
10
|
{# <a href="{{ url_for('control.disconnect', instrument=instrument) }}" class="stretched-link controller-card" style="float: right;color: red; position: relative;">Disconnect <i class="bi bi-x-square"></i></a>#}
|
|
11
11
|
<div class="p-4 controller-card">
|
|
12
|
-
<h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a></h5>
|
|
12
|
+
<h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument.split(".")[1]}}</a></h5>
|
|
13
13
|
</div>
|
|
14
14
|
{# {% else %}#}
|
|
15
15
|
{# <div class="p-4 controller-card">#}
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
{% endfor %}
|
|
22
|
+
<div class="d-flex mb-3">
|
|
23
|
+
<a href="{{ url_for('design.download', filetype='proxy') }}" class="btn btn-outline-primary">
|
|
24
|
+
<i class="bi bi-download"></i> Download remote control script
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
22
27
|
{% if not deck %}
|
|
23
28
|
<div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
|
|
24
29
|
<div class="bg-white rounded shadow-sm position-relative">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app
|
|
1
|
+
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
|
|
2
2
|
from flask_login import login_required
|
|
3
3
|
|
|
4
|
-
from ivoryos.utils.db_models import Script, db
|
|
4
|
+
from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
|
|
5
5
|
from ivoryos.utils.utils import get_script_file, post_script_file
|
|
6
6
|
|
|
7
7
|
database = Blueprint('database', __name__, template_folder='templates/database')
|
|
@@ -188,3 +188,60 @@ def save_as():
|
|
|
188
188
|
else:
|
|
189
189
|
flash("Script name is already exist in database")
|
|
190
190
|
return redirect(url_for("design.experiment_builder"))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@database.route('/workflow_runs')
|
|
194
|
+
def list_workflows():
|
|
195
|
+
query = WorkflowRun.query
|
|
196
|
+
search_term = request.args.get("keyword", None)
|
|
197
|
+
if search_term:
|
|
198
|
+
query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
|
|
199
|
+
page = request.args.get('page', default=1, type=int)
|
|
200
|
+
per_page = 10
|
|
201
|
+
|
|
202
|
+
workflows = query.paginate(page=page, per_page=per_page, error_out=False)
|
|
203
|
+
return render_template('workflow_run_database.html', workflows=workflows)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@database.route("/workflow_steps/<int:workflow_id>")
|
|
207
|
+
def get_workflow_steps(workflow_id):
|
|
208
|
+
workflow = WorkflowRun.query.get_or_404(workflow_id)
|
|
209
|
+
steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
|
|
210
|
+
|
|
211
|
+
# Organize steps by phase + repeat_index
|
|
212
|
+
grouped = {
|
|
213
|
+
"prep": [],
|
|
214
|
+
"script": {},
|
|
215
|
+
"cleanup": [],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for step in steps:
|
|
219
|
+
if step.phase == "prep":
|
|
220
|
+
grouped["prep"].append(step)
|
|
221
|
+
elif step.phase == "script":
|
|
222
|
+
grouped["script"].setdefault(step.repeat_index, []).append(step)
|
|
223
|
+
elif step.phase == "cleanup" or step.method_name == "stop":
|
|
224
|
+
grouped["cleanup"].append(step)
|
|
225
|
+
|
|
226
|
+
return render_template("experiment_step_view.html", workflow=workflow, grouped=grouped)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@database.route("/delete_workflow_data/<workflow_id>")
|
|
230
|
+
@login_required
|
|
231
|
+
def delete_workflow_data(workflow_id: str):
|
|
232
|
+
"""
|
|
233
|
+
.. :quickref: Database; delete experiment data from database
|
|
234
|
+
|
|
235
|
+
delete workflow data from database
|
|
236
|
+
|
|
237
|
+
.. http:get:: /delete_workflow_data/<workflow_id>
|
|
238
|
+
|
|
239
|
+
:param workflow_id: workflow id
|
|
240
|
+
:type workflow_id: str
|
|
241
|
+
:status 302: redirect to :http:get:`/ivoryos/workflow_runs/`
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
run = WorkflowRun.query.get(workflow_id)
|
|
245
|
+
db.session.delete(run)
|
|
246
|
+
db.session.commit()
|
|
247
|
+
return redirect(url_for('database.list_workflows'))
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<th scope="col">Time created</th>
|
|
31
31
|
<th scope="col">Last modified</th>
|
|
32
32
|
<th scope="col">Author</th>
|
|
33
|
-
<th scope="col">Registered</th
|
|
33
|
+
{# <th scope="col">Registered</th>#}
|
|
34
34
|
<th scope="col"></th>
|
|
35
35
|
</tr>
|
|
36
36
|
</thead>
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
<td>{{ workflow.time_created }}</td>
|
|
44
44
|
<td>{{ workflow.last_modified }}</td>
|
|
45
45
|
<td>{{ workflow.author }}</td>
|
|
46
|
-
<td>{{ workflow.registered }}</td
|
|
46
|
+
{# <td>{{ workflow.registered }}</td>#}
|
|
47
47
|
<td>
|
|
48
48
|
{#not workflow.status == "finalized" or#}
|
|
49
49
|
{% if session['user'] == 'admin' or session['user'] == workflow.author %}
|
|
50
50
|
<a href="{{ url_for('database.delete_workflow', workflow_name=workflow.name) }}">delete</a>
|
|
51
51
|
{% else %}
|
|
52
|
-
<a class="disabled-link"
|
|
52
|
+
<a class="disabled-link">delete</a>
|
|
53
53
|
{% endif %}
|
|
54
54
|
<td>
|
|
55
55
|
</tr>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block title %}IvoryOS | Experiment Results{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block body %}
|
|
6
|
+
<style>
|
|
7
|
+
.vis-time-axis .vis-text.vis-minor,
|
|
8
|
+
.vis-time-axis .vis-text.vis-major {
|
|
9
|
+
color: #666;
|
|
10
|
+
}
|
|
11
|
+
.vis-item.stop {
|
|
12
|
+
background-color: red;
|
|
13
|
+
color: white;
|
|
14
|
+
border: none;
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
|
|
19
|
+
<div id="timeline"></div>
|
|
20
|
+
|
|
21
|
+
<script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
|
22
|
+
<link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
|
|
23
|
+
|
|
24
|
+
<h1>Experiment Step View</h1>
|
|
25
|
+
|
|
26
|
+
<div id="visualization"></div>
|
|
27
|
+
|
|
28
|
+
<script type="text/javascript">
|
|
29
|
+
var container = document.getElementById('visualization');
|
|
30
|
+
|
|
31
|
+
const items = [
|
|
32
|
+
{% if grouped.prep %}
|
|
33
|
+
{
|
|
34
|
+
id: 'prep',
|
|
35
|
+
content: 'Prep Phase',
|
|
36
|
+
start: '{{ grouped.prep[0].start_time }}',
|
|
37
|
+
end: '{{ grouped.prep[-1].end_time }}',
|
|
38
|
+
className: 'prep',
|
|
39
|
+
group: 'prep'
|
|
40
|
+
},
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
44
|
+
{
|
|
45
|
+
id: 'iter{{ repeat_index }}',
|
|
46
|
+
content: 'Iteration {{ repeat_index }}',
|
|
47
|
+
start: '{{ step_list[0].start_time }}',
|
|
48
|
+
end: '{{ step_list[-1].end_time }}',
|
|
49
|
+
className: 'script',
|
|
50
|
+
group: 'iter{{ repeat_index }}'
|
|
51
|
+
},
|
|
52
|
+
{% for step in step_list %}
|
|
53
|
+
{% if step.method_name == "stop" %}
|
|
54
|
+
{
|
|
55
|
+
id: 'stop-{{ step.id }}',
|
|
56
|
+
content: '🛑 Stop',
|
|
57
|
+
start: '{{ step.start_time }}',
|
|
58
|
+
type: 'point',
|
|
59
|
+
className: 'stop',
|
|
60
|
+
group: 'iter{{ repeat_index }}'
|
|
61
|
+
},
|
|
62
|
+
{% endif %}
|
|
63
|
+
{% endfor %}
|
|
64
|
+
{% endfor %}
|
|
65
|
+
|
|
66
|
+
{% if grouped.cleanup %}
|
|
67
|
+
{
|
|
68
|
+
id: 'cleanup',
|
|
69
|
+
content: 'Cleanup Phase',
|
|
70
|
+
start: '{{ grouped.cleanup[0].start_time }}',
|
|
71
|
+
end: '{{ grouped.cleanup[-1].end_time }}',
|
|
72
|
+
className: 'cleanup',
|
|
73
|
+
group: 'cleanup'
|
|
74
|
+
|
|
75
|
+
},
|
|
76
|
+
{% endif %}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const groups = [
|
|
80
|
+
{% if grouped.prep %}{ id: 'prep', content: 'Prep' },{% endif %}
|
|
81
|
+
{% for repeat_index in grouped.script.keys()|sort %}{ id: 'iter{{ repeat_index }}', content: 'Iteration {{ repeat_index }}' },{% endfor %}
|
|
82
|
+
{% if grouped.cleanup %}{ id: 'cleanup', content: 'Cleanup' },{% endif %}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
var options = {
|
|
86
|
+
clickToUse: true,
|
|
87
|
+
stack: false, // important to keep point within group row
|
|
88
|
+
horizontalScroll: true,
|
|
89
|
+
zoomKey: 'ctrlKey'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Initialize your timeline with the sorted groups
|
|
93
|
+
const timeline = new vis.Timeline(container, items, groups, options);
|
|
94
|
+
|
|
95
|
+
timeline.on('select', function (props) {
|
|
96
|
+
const id = props.items[0];
|
|
97
|
+
if (id && id.startsWith('iter')) {
|
|
98
|
+
const card = document.getElementById('card-' + id);
|
|
99
|
+
if (card) {
|
|
100
|
+
const yOffset = -80;
|
|
101
|
+
const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
|
102
|
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<h2>Workflow: {{ workflow.name }}</h2>
|
|
109
|
+
|
|
110
|
+
{% if grouped.prep %}
|
|
111
|
+
<h4 class="mt-4">Prep Phase</h4>
|
|
112
|
+
{% for step in grouped.prep %}
|
|
113
|
+
{% include "step_card.html" %}
|
|
114
|
+
{% endfor %}
|
|
115
|
+
{% endif %}
|
|
116
|
+
|
|
117
|
+
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
118
|
+
<h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
|
|
119
|
+
{% for step in step_list %}
|
|
120
|
+
{% include "step_card.html" %}
|
|
121
|
+
{% endfor %}
|
|
122
|
+
{% endfor %}
|
|
123
|
+
|
|
124
|
+
{% if grouped.cleanup %}
|
|
125
|
+
<h4 class="mt-4">Cleanup Phase</h4>
|
|
126
|
+
{% for step in grouped.cleanup %}
|
|
127
|
+
{% include "step_card.html" %}
|
|
128
|
+
{% endfor %}
|
|
129
|
+
{% endif %}
|
|
130
|
+
{% endblock %}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="card mb-2 {{ 'border-danger text-danger bg-light' if step.run_error else 'border-secondary' }}">
|
|
2
|
+
<div class="card-body p-2">
|
|
3
|
+
<strong>{{ step.method_name }}</strong>
|
|
4
|
+
<small>Start: {{ step.start_time }}</small>
|
|
5
|
+
<small>End: {{ step.end_time }}</small>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block title %}IvoryOS | Design Database{% endblock %}
|
|
4
|
+
{% block body %}
|
|
5
|
+
|
|
6
|
+
<table class="table table-hover" id="workflowResultLibrary">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th scope="col">Workflow name</th>
|
|
10
|
+
<th scope="col">Start time</th>
|
|
11
|
+
<th scope="col">End time</th>
|
|
12
|
+
<th scope="col">Data</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
{% for workflow in workflows %}
|
|
17
|
+
<tr>
|
|
18
|
+
<td><a href="{{ url_for('database.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
|
|
19
|
+
<td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
|
|
20
|
+
<td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
|
|
21
|
+
|
|
22
|
+
<td>
|
|
23
|
+
{% if workflow.data_path %}
|
|
24
|
+
<a href="{{ url_for('design.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
|
|
25
|
+
{% endif %}
|
|
26
|
+
</td>
|
|
27
|
+
<td>
|
|
28
|
+
{% if session['user'] == 'admin' or session['user'] == workflow.author %}
|
|
29
|
+
<a href="{{ url_for('database.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
|
|
30
|
+
{% else %}
|
|
31
|
+
<a class="disabled-link">delete</a>
|
|
32
|
+
{% endif %}
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
|
|
39
|
+
{# paging#}
|
|
40
|
+
<div class="pagination justify-content-center">
|
|
41
|
+
<div class="page-item {{ 'disabled' if not workflows.has_prev else '' }}">
|
|
42
|
+
<a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.prev_num) }}">Previous</a>
|
|
43
|
+
</div>
|
|
44
|
+
{% for num in workflows.iter_pages() %}
|
|
45
|
+
<div class="page-item">
|
|
46
|
+
<a class="page-link {{ 'active' if num == workflows.page else '' }}" href="{{ url_for('database.list_workflows', page=num) }}">{{ num }}</a>
|
|
47
|
+
</div>
|
|
48
|
+
{% endfor %}
|
|
49
|
+
<div class="page-item {{ 'disabled' if not workflows.has_next else '' }}">
|
|
50
|
+
<a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.next_num) }}">Next</a>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div id="steps-container"></div>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
function showSteps(workflowId) {
|
|
58
|
+
fetch(`/workflow_steps/${workflowId}`)
|
|
59
|
+
.then(response => response.json())
|
|
60
|
+
.then(data => {
|
|
61
|
+
const container = document.getElementById('steps-container');
|
|
62
|
+
container.innerHTML = ''; // Clear previous content
|
|
63
|
+
const stepsList = document.createElement('ul');
|
|
64
|
+
|
|
65
|
+
data.steps.forEach(step => {
|
|
66
|
+
const li = document.createElement('li');
|
|
67
|
+
li.innerHTML = `
|
|
68
|
+
<strong>Step: </strong> ${step.method_name} <br>
|
|
69
|
+
<strong>Start Time:</strong> ${step.start_time} <br>
|
|
70
|
+
<strong>End Time:</strong> ${step.end_time} <br>
|
|
71
|
+
<strong>Human Intervention:</strong> ${step.run_error ? 'Yes' : 'No'}
|
|
72
|
+
`;
|
|
73
|
+
stepsList.appendChild(li);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
container.appendChild(stepsList);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
{% endblock %}
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -11,6 +11,7 @@ from flask_socketio import SocketIO
|
|
|
11
11
|
from werkzeug.utils import secure_filename
|
|
12
12
|
|
|
13
13
|
from ivoryos.utils import utils
|
|
14
|
+
from ivoryos.utils.client_proxy import create_function, export_to_python
|
|
14
15
|
from ivoryos.utils.global_config import GlobalConfig
|
|
15
16
|
from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
|
|
16
17
|
create_form_from_action, create_all_builtin_forms
|
|
@@ -39,6 +40,13 @@ def handle_abort_current():
|
|
|
39
40
|
|
|
40
41
|
@socketio.on('pause')
|
|
41
42
|
def handle_pause():
|
|
43
|
+
runner.retry = False
|
|
44
|
+
msg = runner.toggle_pause()
|
|
45
|
+
socketio.emit('log', {'message': msg})
|
|
46
|
+
|
|
47
|
+
@socketio.on('retry')
|
|
48
|
+
def handle_pause():
|
|
49
|
+
runner.retry = True
|
|
42
50
|
msg = runner.toggle_pause()
|
|
43
51
|
socketio.emit('log', {'message': msg})
|
|
44
52
|
|
|
@@ -360,7 +368,7 @@ def experiment_run():
|
|
|
360
368
|
run_name = script.validate_function_name(run_name)
|
|
361
369
|
runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
|
|
362
370
|
logger=g.logger, socketio=g.socketio, repeat_count=repeat,
|
|
363
|
-
output_path=datapath
|
|
371
|
+
output_path=datapath, current_app=current_app._get_current_object()
|
|
364
372
|
)
|
|
365
373
|
if utils.check_config_duplicate(config):
|
|
366
374
|
flash(f"WARNING: Duplicate in config entries.")
|
|
@@ -525,7 +533,20 @@ def download(filetype):
|
|
|
525
533
|
outfile.write(json_object)
|
|
526
534
|
elif filetype == "python":
|
|
527
535
|
filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
|
|
528
|
-
|
|
536
|
+
elif filetype == "proxy":
|
|
537
|
+
snapshot = global_config.deck_snapshot.copy()
|
|
538
|
+
class_definitions = {}
|
|
539
|
+
# Iterate through each instrument in the snapshot
|
|
540
|
+
for instrument_key, instrument_data in snapshot.items():
|
|
541
|
+
# Iterate through each function associated with the current instrument
|
|
542
|
+
for function_key, function_data in instrument_data.items():
|
|
543
|
+
# Convert the function signature to a string representation
|
|
544
|
+
function_data['signature'] = str(function_data['signature'])
|
|
545
|
+
class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
|
|
546
|
+
class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
|
|
547
|
+
# Export the generated class definitions to a .py script
|
|
548
|
+
export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
|
|
549
|
+
filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
|
|
529
550
|
return send_file(os.path.abspath(filepath), as_attachment=True)
|
|
530
551
|
|
|
531
552
|
|
|
@@ -144,6 +144,16 @@
|
|
|
144
144
|
{{ field(class="btn btn-dark") }}
|
|
145
145
|
{% elif field.type == "BooleanField" %}
|
|
146
146
|
{{ field(class="form-check-input") }}
|
|
147
|
+
{% elif field.type == "FlexibleEnumField" %}
|
|
148
|
+
<input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
|
|
149
|
+
list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
|
|
150
|
+
class="form-control">
|
|
151
|
+
<datalist id="{{ field.id }}_options">
|
|
152
|
+
{% for key in field.choices %}
|
|
153
|
+
<option value="{{ key }}">{{ key }}</option>
|
|
154
|
+
{% endfor %}
|
|
155
|
+
</datalist>
|
|
156
|
+
|
|
147
157
|
{% else %}
|
|
148
158
|
{{ field(class="form-control") }}
|
|
149
159
|
{% endif %}
|
|
@@ -219,7 +229,7 @@
|
|
|
219
229
|
|
|
220
230
|
{# canvas #}
|
|
221
231
|
<div class="col-md-9 scroll-column">
|
|
222
|
-
<div>
|
|
232
|
+
<div class="d-flex align-items-center ">
|
|
223
233
|
{# file dropdown menu #}
|
|
224
234
|
<ul class="nav nav-tabs">
|
|
225
235
|
<li class="nav-item dropdown">
|
|
@@ -242,6 +252,12 @@
|
|
|
242
252
|
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='script' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='script') }}">Experiment</a></li>
|
|
243
253
|
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='cleanup' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='cleanup') }}">Clean up</a></li>
|
|
244
254
|
</ul>
|
|
255
|
+
|
|
256
|
+
<div class="form-check form-switch ms-auto">
|
|
257
|
+
<input class="form-check-input" type="checkbox" id="toggleLineNumbers" onchange="toggleLineNumbers()">
|
|
258
|
+
<label class="form-check-label" for="toggleLineNumbers">Show Line Numbers</label>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
245
261
|
</div>
|
|
246
262
|
|
|
247
263
|
<div class="canvas" droppable="true">
|
|
@@ -279,6 +295,7 @@
|
|
|
279
295
|
<ul class="reorder">
|
|
280
296
|
{% for button in buttons %}
|
|
281
297
|
<li id="{{ button['id'] }}" style="list-style-type: none;">
|
|
298
|
+
<span class="line-number d-none">{{ button['id'] }}.</span>
|
|
282
299
|
<a href="{{ url_for('design.edit_action', uuid=button['uuid']) }}" type="button" class="btn btn-light" style="{{ button['style'] }}">{{ button['label'] }}</a>
|
|
283
300
|
{% if not button["instrument"] in ["if","while","repeat"] %}
|
|
284
301
|
<a href="{{ url_for('design.duplicate_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-copy"></span></a>
|
|
@@ -406,14 +423,44 @@
|
|
|
406
423
|
</div>
|
|
407
424
|
|
|
408
425
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
</script>
|
|
413
|
-
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
|
414
|
-
{% endif %}
|
|
415
|
-
<script>
|
|
416
|
-
const updateListUrl = "{{ url_for('design.update_list') }}";
|
|
426
|
+
{% if instrument and use_llm %}
|
|
427
|
+
<script>
|
|
428
|
+
const buttonIds = {{ ['generate'] | tojson }};
|
|
417
429
|
</script>
|
|
418
|
-
<script src="{{ url_for('static', filename='js/
|
|
430
|
+
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
|
431
|
+
{% endif %}
|
|
432
|
+
|
|
433
|
+
<script>
|
|
434
|
+
const updateListUrl = "{{ url_for('design.update_list') }}";
|
|
435
|
+
|
|
436
|
+
// Toggle visibility of line numbers
|
|
437
|
+
function toggleLineNumbers(save = true) {
|
|
438
|
+
const show = document.getElementById('toggleLineNumbers').checked;
|
|
439
|
+
document.querySelectorAll('.line-number').forEach(el => {
|
|
440
|
+
el.classList.toggle('d-none', !show);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (save) {
|
|
444
|
+
localStorage.setItem('showLineNumbers', show ? 'true' : 'false');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Restore state on page load
|
|
449
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
450
|
+
const savedState = localStorage.getItem('showLineNumbers');
|
|
451
|
+
const checkbox = document.getElementById('toggleLineNumbers');
|
|
452
|
+
|
|
453
|
+
if (savedState === 'true') {
|
|
454
|
+
checkbox.checked = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
toggleLineNumbers(false); // don't overwrite localStorage on load
|
|
458
|
+
|
|
459
|
+
checkbox.addEventListener('change', () => toggleLineNumbers());
|
|
460
|
+
});
|
|
461
|
+
</script>
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
<script src="{{ url_for('static', filename='js/sortable_design.js') }}"></script>
|
|
465
|
+
|
|
419
466
|
{% endblock %}
|