ivoryos 0.1.22__py3-none-any.whl → 0.1.24__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
@@ -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
@@ -81,7 +81,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
81
81
  config: Config = None,
82
82
  logger: Union[str, list] = None,
83
83
  logger_output_name: str = None,
84
- enable_design=True
84
+ enable_design=True,
85
+ blueprint_plugins=None,
85
86
  ):
86
87
  """
87
88
  Start ivoryOS app server.
@@ -99,7 +100,11 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
99
100
  """
100
101
  app = create_app(config_class=config or get_config()) # Create app instance using factory function
101
102
 
102
- plugins = load_plugins(app, socketio)
103
+ # plugins = load_installed_plugins(app, socketio)
104
+ plugins = {}
105
+ if blueprint_plugins:
106
+ config_plugins = load_plugins(blueprint_plugins, app, socketio)
107
+ plugins.extend(config_plugins)
103
108
 
104
109
  def inject_nav_config():
105
110
  """Make NAV_CONFIG available globally to all templates."""
@@ -143,7 +148,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
143
148
  # return app
144
149
 
145
150
 
146
- def load_plugins(app, socketio):
151
+ def load_installed_plugins(app, socketio):
147
152
  """
148
153
  Dynamically load installed plugins and attach Flask-SocketIO.
149
154
  """
@@ -160,4 +165,17 @@ def load_plugins(app, socketio):
160
165
 
161
166
  return plugin_names
162
167
 
163
-
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
@@ -203,11 +203,27 @@ def list_workflows():
203
203
  return render_template('workflow_run_database.html', workflows=workflows)
204
204
 
205
205
 
206
- @database.route('/workflow_steps/<int:workflow_id>')
206
+ @database.route("/workflow_steps/<int:workflow_id>")
207
207
  def get_workflow_steps(workflow_id):
208
- steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).all()
209
- steps_data = [step.as_dict() for step in steps]
210
- return jsonify({'steps': steps_data})
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)
211
227
 
212
228
 
213
229
  @database.route("/delete_workflow_data/<workflow_id>")
@@ -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>
@@ -40,6 +40,13 @@ def handle_abort_current():
40
40
 
41
41
  @socketio.on('pause')
42
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
43
50
  msg = runner.toggle_pause()
44
51
  socketio.emit('log', {'message': msg})
45
52
 
@@ -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
- {% if instrument and use_llm %}
410
- <script>
411
- const buttonIds = {{ ['generate'] | tojson }};
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/sortable_design.js') }}"></script>
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 %}
@@ -423,8 +423,9 @@
423
423
  <p>Do you want to continue execution or stop?</p>
424
424
  </div>
425
425
  <div class="modal-footer">
426
- <button type="button" class="btn btn-danger" id="stop-btn" data-bs-dismiss="modal">Stop Execution</button>
427
- <button type="button" class="btn btn-success" id="continue-btn" data-bs-dismiss="modal">Continue</button>
426
+ <button type="button" class="btn btn-primary" id="retry-btn" data-bs-dismiss="modal">Rerun Current Step</button>
427
+ <button type="button" class="btn btn-success" id="continue-btn" data-bs-dismiss="modal">Continue</button>
428
+ <button type="button" class="btn btn-danger" id="stop-btn" data-bs-dismiss="modal">Stop Execution</button>
428
429
  </div>
429
430
  </div>
430
431
  </div>
@@ -68,6 +68,11 @@ document.addEventListener("DOMContentLoaded", function() {
68
68
  console.log("Execution resumed.");
69
69
  });
70
70
 
71
+ document.getElementById('retry-btn').addEventListener('click', function() {
72
+ socket.emit('retry'); // Resume execution
73
+ console.log("Execution resumed, retrying.");
74
+ });
75
+
71
76
  document.getElementById('stop-btn').addEventListener('click', function() {
72
77
  socket.emit('pause'); // Resume execution
73
78
  socket.emit('abort_current'); // Stop execution
ivoryos/utils/form.py CHANGED
@@ -1,6 +1,8 @@
1
+ from enum import Enum
2
+
1
3
  from wtforms.fields.choices import SelectField
2
4
  from wtforms.fields.core import Field
3
- from wtforms.validators import InputRequired
5
+ from wtforms.validators import InputRequired, ValidationError
4
6
  from wtforms.widgets.core import TextInput
5
7
 
6
8
  from flask_wtf import FlaskForm
@@ -187,6 +189,30 @@ class VariableOrBoolField(BooleanField):
187
189
  return "y"
188
190
 
189
191
 
192
+ class FlexibleEnumField(StringField):
193
+ def __init__(self, label=None, validators=None, choices=None, script=None, **kwargs):
194
+ super().__init__(label, validators, **kwargs)
195
+ self.script = script
196
+ self.enum_class = choices
197
+ self.choices = [e.name for e in self.enum_class]
198
+ # self.value_list = [e.name for e in self.enum_class]
199
+
200
+
201
+ def process_formdata(self, valuelist):
202
+ if valuelist:
203
+ key = valuelist[0]
204
+ if key in self.choices:
205
+ # Convert the string key to Enum instance
206
+ self.data = self.enum_class[key].value
207
+ elif self.data.startswith("#"):
208
+ if not self.script.editing_type == "script":
209
+ raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
210
+ self.data = self.data
211
+ else:
212
+ raise ValidationError(
213
+ f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
214
+
215
+
190
216
  def format_name(name):
191
217
  """Converts 'example_name' to 'Example Name'."""
192
218
  name = name.split(".")[-1]
@@ -218,20 +244,39 @@ def create_form_for_method(method, autofill, script=None, design=True):
218
244
  if param.name == 'self':
219
245
  continue
220
246
  formatted_param_name = format_name(param.name)
247
+
248
+ default_value = None
249
+ if autofill:
250
+ default_value = f'#{param.name}'
251
+ else:
252
+ if param.default is not param.empty:
253
+ if isinstance(param.default, Enum):
254
+ default_value = param.default.name
255
+ else:
256
+ default_value = param.default
257
+
221
258
  field_kwargs = {
222
259
  "label": formatted_param_name,
223
- "default": f'#{param.name}' if autofill else (param.default if param.default is not param.empty else None),
260
+ "default": default_value,
224
261
  "validators": [InputRequired()] if param.default is param.empty else None,
225
262
  **({"script": script} if (autofill or design) else {})
226
263
  }
227
- field_class, placeholder_text = annotation_mapping.get(
228
- param.annotation,
229
- (VariableOrStringField if design else StringField, f'Enter {param.annotation} value')
230
- )
264
+ if isinstance(param.annotation, type) and issubclass(param.annotation, Enum):
265
+ # enum_class = [(e.name, e.value) for e in param.annotation]
266
+ field_class = FlexibleEnumField
267
+ placeholder_text = f"Choose or type a value for {param.annotation.__name__} (start with # for custom)"
268
+ extra_kwargs = {"choices": param.annotation}
269
+ else:
270
+ field_class, placeholder_text = annotation_mapping.get(
271
+ param.annotation,
272
+ (VariableOrStringField if design else StringField, f'Enter {param.annotation} value')
273
+ )
274
+ extra_kwargs = {}
275
+
231
276
  render_kwargs = {"placeholder": placeholder_text}
232
277
 
233
278
  # Create the field with additional rendering kwargs for placeholder text
234
- field = field_class(**field_kwargs, render_kw=render_kwargs)
279
+ field = field_class(**field_kwargs, render_kw=render_kwargs, **extra_kwargs)
235
280
  setattr(DynamicForm, param.name, field)
236
281
 
237
282
  # setattr(DynamicForm, f'add', fname)
@@ -17,6 +17,7 @@ deck = None
17
17
 
18
18
  class ScriptRunner:
19
19
  def __init__(self, globals_dict=None):
20
+ self.retry = False
20
21
  if globals_dict is None:
21
22
  globals_dict = globals()
22
23
  self.globals_dict = globals_dict
@@ -143,7 +144,12 @@ class ScriptRunner:
143
144
  # if line.startswith("registered_workflows"):
144
145
  # line = line.replace("registered_workflows.", "")
145
146
  try:
146
- exec(line, exec_globals, exec_locals)
147
+ if line.startswith("time.sleep("): # add safe sleep for time.sleep lines
148
+ duration_str = line[len("time.sleep("):-1]
149
+ duration = float(duration_str)
150
+ self.safe_sleep(duration)
151
+ else:
152
+ exec(line, exec_globals, exec_locals)
147
153
  step.run_error = False
148
154
  except Exception as e:
149
155
  logger.error(f"Error during script execution: {e}")
@@ -152,14 +158,18 @@ class ScriptRunner:
152
158
  step.run_error = True
153
159
  self.toggle_pause()
154
160
  step.end_time = datetime.now()
161
+ db.session.add(step)
162
+ db.session.commit()
163
+
155
164
  self.pause_event.wait()
156
165
 
157
166
  # todo update script during the run
158
167
  # _func_str = script.compile()
159
168
  # step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
160
- db.session.add(step)
161
- db.session.commit()
162
- index += 1
169
+ if not step.run_error:
170
+ index += 1
171
+ elif not self.retry:
172
+ index += 1
163
173
  return exec_locals # Return the 'results' variable
164
174
 
165
175
  def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
@@ -290,3 +300,11 @@ class ScriptRunner:
290
300
  @staticmethod
291
301
  def _emit_progress(socketio, progress):
292
302
  socketio.emit('progress', {'progress': progress})
303
+
304
+ def safe_sleep(self, duration: float):
305
+ interval = 1 # check every 1 second
306
+ end_time = time.time() + duration
307
+ while time.time() < end_time:
308
+ if self.stop_current_event.is_set():
309
+ return # Exit early if stop is requested
310
+ time.sleep(min(interval, end_time - time.time()))
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.22"
1
+ __version__ = "0.1.24"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivoryos
3
- Version: 0.1.22
3
+ Version: 0.1.24
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Home-page: https://gitlab.com/heingroup/ivoryos
6
6
  Author: Ivory Zhang
@@ -1,6 +1,6 @@
1
- ivoryos/__init__.py,sha256=MVzYifnYAyHZvr0PfMs9ck3Nf4Xfj3fJz4djCq3rcC4,6081
1
+ ivoryos/__init__.py,sha256=mJ55XraFFw7g_771ur2KVL2gCjR4jlZyTkLjUZ73BFI,6895
2
2
  ivoryos/config.py,sha256=3FPBYTIBhQTKDvsEoR8ZeTmg65D-CSFEdGmOuIL4pSI,1311
3
- ivoryos/version.py,sha256=zmP2TRnzKPjZJ1eiBcT-cRInsji6FW-OVD3FafQFCc4,23
3
+ ivoryos/version.py,sha256=Jq7e1LcKcQSNVg4EOJ-acPyPgs8Os5cYEZWXrQsI7Pg,23
4
4
  ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  ivoryos/routes/auth/auth.py,sha256=7CdXjGAr1B_xsmwweakTWOoROgsOJf0MNTzlMP_5Nus,3240
@@ -12,13 +12,15 @@ ivoryos/routes/control/templates/control/controllers.html,sha256=iIp0h6WA68gQj9O
12
12
  ivoryos/routes/control/templates/control/controllers_home.html,sha256=VQ77HRvBlyBrQ3al5fcKF5Y6_vKtU8WeAhilqQQltAo,2997
13
13
  ivoryos/routes/control/templates/control/controllers_new.html,sha256=uOQo9kYmwX2jk3KZDkMUF_ylfNUIs_oIWb_kk_MMVDM,4921
14
14
  ivoryos/routes/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- ivoryos/routes/database/database.py,sha256=UY-6tHKPZaDch0whXVwgu201SQ231tPCE8ej374GOZ8,7140
15
+ ivoryos/routes/database/database.py,sha256=uchoLXRx8ShJZIu20NZya6FJ4LMg2b9yvn6hjEq3dlU,7687
16
16
  ivoryos/routes/database/templates/database/experiment_database.html,sha256=edlCcKfrS91gGG1dPFQjC9xD7F7nWNNqS3S6Oa7apzs,3460
17
+ ivoryos/routes/database/templates/database/experiment_step_view.html,sha256=u8_XYhiZ98PzglMzFEkuM1Tk9hVWf79xXIrpHVDxKa0,3618
18
+ ivoryos/routes/database/templates/database/step_card.html,sha256=F4JRfacrEQfk2rrEbcI_i7G84nzKKDmCrMSmStLb4W4,290
17
19
  ivoryos/routes/database/templates/database/workflow_run_database.html,sha256=MczK9my9u0SyQsMFLbc6CXeZqKaBo5vk1SpwjkcZdqk,3571
18
20
  ivoryos/routes/design/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- ivoryos/routes/design/design.py,sha256=HxEGPL5KTG2-roOB7UB9Ph28haY37IS5irKfloNEld0,25054
20
- ivoryos/routes/design/templates/design/experiment_builder.html,sha256=2cT25URI6NoRDKuU6xl8Vsv4pqyQhhx-Fu8JOfPIP8k,27110
21
- ivoryos/routes/design/templates/design/experiment_run.html,sha256=2CWbHx2M4h4EW7E0EapGPtwza56VEqhZDAIX4IDzYjQ,30266
21
+ ivoryos/routes/design/design.py,sha256=gO8FKuSdGD6EiN3cbspk7jv6y2Ar25A-ZdrCyUTInpE,25221
22
+ ivoryos/routes/design/templates/design/experiment_builder.html,sha256=rEdcHj5onJG_4MejdFBPnJVzsvCMp1KDteqNkpx24kQ,29430
23
+ ivoryos/routes/design/templates/design/experiment_run.html,sha256=Q7cYYTgvZ8SBzqkDEhAwR634-LLcYq4Gof4bpH_adt0,30397
22
24
  ivoryos/routes/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
25
  ivoryos/routes/main/main.py,sha256=yuVJzXAob1kc1dfflkTBIZQ0tdf6kChfuq-uQlN1e9Q,957
24
26
  ivoryos/routes/main/templates/main/help.html,sha256=IOktMEsOPk0SCiMBXZ4mpffClERAyX8W82fel71M3M0,9370
@@ -29,20 +31,20 @@ ivoryos/static/style.css,sha256=zQVx35A5g6JMJ-K84-6fSKtzXGjp_p5ZVG6KLHPM2IE,4021
29
31
  ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
30
32
  ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
31
33
  ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
32
- ivoryos/static/js/socket_handler.js,sha256=fJ_SKCfS2ZejnECPKZuD8DTEPhBxmm0m-yCvxf2UtIQ,4885
34
+ ivoryos/static/js/socket_handler.js,sha256=YGwWlT8TqNBvvIzs2G9g1g7nM2-vUPZjCwmQt4Yv0Uw,5078
33
35
  ivoryos/static/js/sortable_card.js,sha256=ifmlGe3yy0U_KzMphV4ClRhK2DLOvkELYMlq1vECuac,807
34
36
  ivoryos/static/js/sortable_design.js,sha256=wwpKfIzZGDxfX3moNz0cvPvm9YyHmopZK3wmkUdnBiw,4333
35
37
  ivoryos/templates/base.html,sha256=sDdwqOIUP2Get-py4E59PkieoGWLFpX6wAJe93s4aRo,8518
36
38
  ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  ivoryos/utils/client_proxy.py,sha256=AzcSQGMqeCqVULP1a7vEKNe135NZYryVX63ke0wgK04,2099
38
40
  ivoryos/utils/db_models.py,sha256=1G2De1nLRCGwuY4zqgMeIQ-p1XJ_PkBxH1cd0fJ9YgY,26740
39
- ivoryos/utils/form.py,sha256=Eqrg4G44hoG6TizxTEP4DNd-rEoyN77t9Y7cB1WdrcQ,19205
41
+ ivoryos/utils/form.py,sha256=b3JKxRc1jN45-bXyfzSJT1lcssUuxT86FhRmNUDv5-U,20973
40
42
  ivoryos/utils/global_config.py,sha256=P0xs_33bZfNQ-D71lCkq7HJyT4ngQWPqUKnkoMrmM8c,1908
41
43
  ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
42
- ivoryos/utils/script_runner.py,sha256=jVQzZQXmeVBNREBr5yP2grAKrO_1PXIlN5Uny87-PfI,12547
44
+ ivoryos/utils/script_runner.py,sha256=iyeYCTPWnukW3F8UsarIdZxFzDnHBq6MH0ydfVGNFaw,13286
43
45
  ivoryos/utils/utils.py,sha256=VP-4Wf-slK2pMe659K5TJLbZXVNvyCD1-3kdfp_COxI,14990
44
- ivoryos-0.1.22.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
45
- ivoryos-0.1.22.dist-info/METADATA,sha256=13bJFUatG0fTMy8ZfocdZvmOdqZvQZYhwu-vL3Q2kEo,6846
46
- ivoryos-0.1.22.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
47
- ivoryos-0.1.22.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
48
- ivoryos-0.1.22.dist-info/RECORD,,
46
+ ivoryos-0.1.24.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
47
+ ivoryos-0.1.24.dist-info/METADATA,sha256=HMQdakIiPEMTxGRKLq7Qz4luRku_SeGoTE1eVHSFtC4,6846
48
+ ivoryos-0.1.24.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
49
+ ivoryos-0.1.24.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
50
+ ivoryos-0.1.24.dist-info/RECORD,,