ivoryos 1.3.8__py3-none-any.whl → 1.4.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.

Files changed (34) hide show
  1. ivoryos/optimizer/ax_optimizer.py +44 -25
  2. ivoryos/optimizer/base_optimizer.py +15 -1
  3. ivoryos/optimizer/baybe_optimizer.py +24 -17
  4. ivoryos/optimizer/nimo_optimizer.py +26 -24
  5. ivoryos/routes/data/data.py +27 -9
  6. ivoryos/routes/data/templates/components/step_card.html +47 -11
  7. ivoryos/routes/data/templates/workflow_view.html +14 -5
  8. ivoryos/routes/design/design.py +31 -1
  9. ivoryos/routes/design/design_step.py +2 -1
  10. ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
  11. ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
  12. ivoryos/routes/design/templates/experiment_builder.html +1 -0
  13. ivoryos/routes/execute/execute.py +36 -7
  14. ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
  15. ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
  16. ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
  17. ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  18. ivoryos/routes/execute/templates/experiment_run.html +0 -264
  19. ivoryos/socket_handlers.py +1 -1
  20. ivoryos/static/js/action_handlers.js +252 -118
  21. ivoryos/static/js/sortable_design.js +1 -0
  22. ivoryos/utils/bo_campaign.py +17 -16
  23. ivoryos/utils/db_models.py +122 -18
  24. ivoryos/utils/decorators.py +1 -0
  25. ivoryos/utils/form.py +22 -5
  26. ivoryos/utils/nest_script.py +314 -0
  27. ivoryos/utils/script_runner.py +447 -147
  28. ivoryos/utils/utils.py +11 -1
  29. ivoryos/version.py +1 -1
  30. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/METADATA +3 -2
  31. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/RECORD +34 -33
  32. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/WHEEL +0 -0
  33. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/licenses/LICENSE +0 -0
  34. {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,118 +1,169 @@
1
+ // ============================================================================
2
+ // STATE MANAGEMENT
3
+ // ============================================================================
1
4
 
5
+ let previousHtmlState = null; // Store previous instrument panel state
6
+ let lastFocusedElement = null; // Track focus for modal management
2
7
 
8
+ // ============================================================================
9
+ // MODE & BATCH MANAGEMENT
10
+ // ============================================================================
3
11
 
12
+ function getMode() {
13
+ return sessionStorage.getItem("mode") || "single";
14
+ }
4
15
 
5
- function saveWorkflow(link) {
6
- const url = link.dataset.postUrl;
16
+ function setMode(mode, triggerUpdate = true) {
17
+ sessionStorage.setItem("mode", mode);
7
18
 
8
- fetch(url, {
9
- method: 'POST',
10
- headers: {
11
- 'Content-Type': 'application/json'
12
- }
13
- })
14
- .then(res => res.json())
15
- .then(data => {
16
- if (data.success) {
17
- // flash a success message
18
- // flash("Workflow saved successfully", "success");
19
- window.location.reload(); // or update the UI dynamically
20
- } else {
21
- alert("Failed to save workflow: " + data.error);
22
- }
23
- })
24
- .catch(err => {
25
- console.error("Save error:", err);
26
- alert("Something went wrong.");
27
- });
19
+ const modeButtons = document.querySelectorAll(".mode-toggle");
20
+ const batchOptions = document.getElementById("batch-options");
21
+
22
+ modeButtons.forEach(b => b.classList.toggle("active", b.dataset.mode === mode));
23
+
24
+ if (batchOptions) {
25
+ batchOptions.style.display = (mode === "batch") ? "inline-flex" : "none";
26
+ }
27
+
28
+ if (triggerUpdate) updateCode();
29
+ }
30
+
31
+ function getBatch() {
32
+ return sessionStorage.getItem("batch") || "sample";
28
33
  }
29
34
 
35
+ function setBatch(batch, triggerUpdate = true) {
36
+ sessionStorage.setItem("batch", batch);
30
37
 
31
- function updateInstrumentPanel(link) {
32
- const url = link.dataset.getUrl;
33
- fetch(url)
34
- .then(res => res.json())
35
- .then(data => {
36
- if (data.html) {
37
- document.getElementById("sidebar-wrapper").innerHTML = data.html;
38
- initializeDragHandlers()
39
- }
40
- })
38
+ const batchButtons = document.querySelectorAll(".batch-toggle");
39
+ batchButtons.forEach(b => b.classList.toggle("active", b.dataset.batch === batch));
40
+
41
+ if (triggerUpdate) updateCode();
41
42
  }
42
43
 
43
- function addMethodToDesign(event, form) {
44
- event.preventDefault(); // Prevent default form submission
44
+ // ============================================================================
45
+ // CODE OVERLAY MANAGEMENT
46
+ // ============================================================================
45
47
 
46
- const formData = new FormData(form);
48
+ async function updateCode() {
49
+ try {
50
+ const params = new URLSearchParams({ mode: getMode(), batch: getBatch() });
51
+ const res = await fetch(scriptCompileUrl + "?" + params.toString());
52
+ if (!res.ok) return;
47
53
 
48
- fetch(form.action, {
49
- method: 'POST',
50
- body: formData
51
- })
52
- .then(response => response.json())
53
- .then(data => {
54
- if (data.success) {
55
- updateActionCanvas(data.html);
56
- hideModal();
57
- } else {
58
- alert("Failed to add method: " + data.error);
54
+ const data = await res.json();
55
+ const codeElem = document.getElementById("python-code");
56
+
57
+ codeElem.removeAttribute("data-highlighted"); // Reset highlight.js flag
58
+ codeElem.textContent = data.code['script'] || "# No code found";
59
+
60
+ if (window.hljs) {
61
+ hljs.highlightElement(codeElem);
59
62
  }
60
- })
61
- .catch(error => console.error('Error:', error));
63
+ } catch (err) {
64
+ console.error("Error updating code:", err);
65
+ }
62
66
  }
63
67
 
68
+ function initializeCodeOverlay() {
69
+ const codeElem = document.getElementById("python-code");
70
+ const copyBtn = document.getElementById("copy-code");
71
+ const downloadBtn = document.getElementById("download-code");
72
+
73
+ if (!copyBtn || !downloadBtn) return; // Elements don't exist
74
+
75
+ // Remove old listeners by cloning (prevents duplicate bindings)
76
+ const newCopyBtn = copyBtn.cloneNode(true);
77
+ const newDownloadBtn = downloadBtn.cloneNode(true);
78
+ copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn);
79
+ downloadBtn.parentNode.replaceChild(newDownloadBtn, downloadBtn);
80
+
81
+ // Copy to clipboard
82
+ newCopyBtn.addEventListener("click", () => {
83
+ navigator.clipboard.writeText(codeElem.textContent)
84
+ .then(() => alert("Code copied!"))
85
+ .catch(err => console.error("Failed to copy", err));
86
+ });
87
+
88
+ // Download current code
89
+ newDownloadBtn.addEventListener("click", () => {
90
+ const blob = new Blob([codeElem.textContent], { type: "text/plain" });
91
+ const url = URL.createObjectURL(blob);
92
+ const a = document.createElement("a");
93
+ a.href = url;
94
+ a.download = "script.py";
95
+ a.click();
96
+ URL.revokeObjectURL(url);
97
+ });
98
+ updateCode();
99
+ }
100
+
101
+ // ============================================================================
102
+ // UI UPDATE FUNCTIONS
103
+ // ============================================================================
64
104
 
65
105
  function updateActionCanvas(html) {
66
106
  document.getElementById("canvas-action-wrapper").innerHTML = html;
67
- initializeCanvas(); // Reinitialize canvas functionality
68
- document.querySelectorAll('#pythonCodeOverlay pre code').forEach((block) => {
69
- hljs.highlightElement(block);
107
+ initializeCanvas();
108
+
109
+ const mode = getMode();
110
+ const batch = getBatch();
111
+
112
+ // Rebind event handlers for mode/batch toggles
113
+ document.querySelectorAll(".mode-toggle").forEach(btn => {
114
+ btn.addEventListener("click", () => setMode(btn.dataset.mode));
115
+ });
116
+ document.querySelectorAll(".batch-toggle").forEach(btn => {
117
+ btn.addEventListener("click", () => setBatch(btn.dataset.batch));
70
118
  });
71
- }
72
119
 
120
+ // Restore toggle UI state (without triggering updates)
121
+ setMode(mode, false);
122
+ setBatch(batch, false);
73
123
 
74
- let lastFocusedElement = null;
124
+ // Reinitialize code overlay buttons
125
+ initializeCodeOverlay();
75
126
 
127
+ }
76
128
 
77
- function hideModal() {
78
- if (document.activeElement) {
79
- document.activeElement.blur();
80
- }
81
- $('#dropModal').modal('hide');
82
- if (lastFocusedElement) {
83
- lastFocusedElement.focus(); // Return focus to the triggering element
84
- }
129
+ function updateInstrumentPanel(link) {
130
+ const url = link.dataset.getUrl;
131
+
132
+ fetch(url)
133
+ .then(res => res.json())
134
+ .then(data => {
135
+ if (data.html) {
136
+ document.getElementById("sidebar-wrapper").innerHTML = data.html;
137
+ initializeDragHandlers();
138
+ }
139
+ })
140
+ .catch(err => console.error("Error updating instrument panel:", err));
85
141
  }
86
142
 
87
- function submitEditForm(event) {
88
- event.preventDefault();
89
- const form = event.target;
90
- const formData = new FormData(form);
143
+ // ============================================================================
144
+ // WORKFLOW MANAGEMENT
145
+ // ============================================================================
91
146
 
92
- fetch(form.action, {
147
+ function saveWorkflow(link) {
148
+ const url = link.dataset.postUrl;
149
+
150
+ fetch(url, {
93
151
  method: 'POST',
94
- body: formData
152
+ headers: {
153
+ 'Content-Type': 'application/json'
154
+ }
95
155
  })
96
- .then(response => response.text())
97
- .then(html => {
98
- if (html) {
99
- // Update only the action list
100
- updateActionCanvas(html);
101
-
102
- if (previousHtmlState) {
103
- document.getElementById('instrument-panel').innerHTML = previousHtmlState;
104
- previousHtmlState = null; // Clear the stored state
105
- }
106
- const parser = new DOMParser();
107
- const doc = parser.parseFromString(html, 'text/html');
108
- const warningDiv = doc.querySelector('#warning');
109
- if (warningDiv && warningDiv.textContent.trim()) {
110
- alert(warningDiv.textContent.trim()); // or use a nicer toast
111
- }
156
+ .then(res => res.json())
157
+ .then(data => {
158
+ if (data.success) {
159
+ window.location.reload();
160
+ } else {
161
+ alert("Failed to save workflow: " + data.error);
112
162
  }
113
163
  })
114
- .catch(error => {
115
- console.error('Error:', error);
164
+ .catch(err => {
165
+ console.error("Save error:", err);
166
+ alert("Something went wrong.");
116
167
  });
117
168
  }
118
169
 
@@ -134,33 +185,26 @@ function clearDraft() {
134
185
  .catch(error => console.error("Failed to clear draft", error));
135
186
  }
136
187
 
188
+ // ============================================================================
189
+ // ACTION MANAGEMENT (CRUD Operations)
190
+ // ============================================================================
137
191
 
192
+ function addMethodToDesign(event, form) {
193
+ event.preventDefault();
138
194
 
195
+ const formData = new FormData(form);
139
196
 
140
- let previousHtmlState = null; // Store the previous state
141
-
142
- function duplicateAction(uuid) {
143
- if (!uuid) {
144
- console.error('Invalid UUID');
145
- return;
146
- }
147
-
148
- fetch(scriptStepDupUrl.replace('0', uuid), {
197
+ fetch(form.action, {
149
198
  method: 'POST',
150
- headers: {
151
- 'Content-Type': 'application/json'
152
- }
199
+ body: formData
153
200
  })
154
-
155
- .then(response => response.text())
156
- .then(html => {
157
- updateActionCanvas(html);
158
-
159
- const parser = new DOMParser();
160
- const doc = parser.parseFromString(html, 'text/html');
161
- const warningDiv = doc.querySelector('#warning');
162
- if (warningDiv && warningDiv.textContent.trim()) {
163
- alert(warningDiv.textContent.trim()); // or use a nicer toast
201
+ .then(response => response.json())
202
+ .then(data => {
203
+ if (data.success) {
204
+ updateActionCanvas(data.html);
205
+ hideModal();
206
+ } else {
207
+ alert("Failed to add method: " + data.error);
164
208
  }
165
209
  })
166
210
  .catch(error => console.error('Error:', error));
@@ -172,10 +216,11 @@ function editAction(uuid) {
172
216
  return;
173
217
  }
174
218
 
219
+ // Store current state for rollback
175
220
  previousHtmlState = document.getElementById('instrument-panel').innerHTML;
176
221
 
177
222
  fetch(scriptStepUrl.replace('0', uuid), {
178
- method: 'GET', // no need for Content-Type on GET
223
+ method: 'GET',
179
224
  headers: {
180
225
  'Content-Type': 'application/json'
181
226
  }
@@ -184,9 +229,9 @@ function editAction(uuid) {
184
229
  if (!response.ok) {
185
230
  return response.json().then(err => {
186
231
  if (err.warning) {
187
- alert(err.warning); // <-- should fire now
232
+ alert(err.warning);
188
233
  }
189
- // restore panel so user isn't stuck
234
+ // Restore panel so user isn't stuck
190
235
  if (previousHtmlState) {
191
236
  document.getElementById('instrument-panel').innerHTML = previousHtmlState;
192
237
  previousHtmlState = null;
@@ -199,6 +244,7 @@ function editAction(uuid) {
199
244
  .then(html => {
200
245
  document.getElementById('instrument-panel').innerHTML = html;
201
246
 
247
+ // Set up back button
202
248
  const backButton = document.getElementById('back');
203
249
  if (backButton) {
204
250
  backButton.addEventListener('click', function(e) {
@@ -213,8 +259,53 @@ function editAction(uuid) {
213
259
  .catch(error => console.error('Error:', error));
214
260
  }
215
261
 
262
+ function submitEditForm(event) {
263
+ event.preventDefault();
264
+
265
+ const form = event.target;
266
+ const formData = new FormData(form);
267
+
268
+ fetch(form.action, {
269
+ method: 'POST',
270
+ body: formData
271
+ })
272
+ .then(response => response.text())
273
+ .then(html => {
274
+ if (html) {
275
+ updateActionCanvas(html);
276
+
277
+ // Restore previous instrument panel state
278
+ if (previousHtmlState) {
279
+ document.getElementById('instrument-panel').innerHTML = previousHtmlState;
280
+ previousHtmlState = null;
281
+ }
216
282
 
283
+ // Check for warnings
284
+ showWarningIfExists(html);
285
+ }
286
+ })
287
+ .catch(error => console.error('Error:', error));
288
+ }
217
289
 
290
+ function duplicateAction(uuid) {
291
+ if (!uuid) {
292
+ console.error('Invalid UUID');
293
+ return;
294
+ }
295
+
296
+ fetch(scriptStepDupUrl.replace('0', uuid), {
297
+ method: 'POST',
298
+ headers: {
299
+ 'Content-Type': 'application/json'
300
+ }
301
+ })
302
+ .then(response => response.text())
303
+ .then(html => {
304
+ updateActionCanvas(html);
305
+ showWarningIfExists(html);
306
+ })
307
+ .catch(error => console.error('Error:', error));
308
+ }
218
309
 
219
310
  function deleteAction(uuid) {
220
311
  if (!uuid) {
@@ -230,21 +321,64 @@ function deleteAction(uuid) {
230
321
  })
231
322
  .then(response => response.text())
232
323
  .then(html => {
233
- // Find the first list element's content and replace it
234
324
  updateActionCanvas(html);
235
- // Optionally, check if a warning element exists
236
- const parser = new DOMParser();
237
- const doc = parser.parseFromString(html, 'text/html');
238
- const warningDiv = doc.querySelector('#warning');
239
- if (warningDiv && warningDiv.textContent.trim()) {
240
- alert(warningDiv.textContent.trim()); // or use a nicer toast
241
- }
325
+ showWarningIfExists(html);
242
326
  })
243
327
  .catch(error => console.error('Error:', error));
244
328
  }
245
329
 
330
+ // ============================================================================
331
+ // MODAL MANAGEMENT
332
+ // ============================================================================
333
+
334
+ function hideModal() {
335
+ if (document.activeElement) {
336
+ document.activeElement.blur();
337
+ }
338
+
339
+ $('#dropModal').modal('hide');
340
+
341
+ if (lastFocusedElement) {
342
+ lastFocusedElement.focus();
343
+ }
344
+ }
345
+
346
+ // ============================================================================
347
+ // UTILITY FUNCTIONS
348
+ // ============================================================================
349
+
350
+ function showWarningIfExists(html) {
351
+ const parser = new DOMParser();
352
+ const doc = parser.parseFromString(html, 'text/html');
353
+ const warningDiv = doc.querySelector('#warning');
354
+
355
+ if (warningDiv && warningDiv.textContent.trim()) {
356
+ alert(warningDiv.textContent.trim());
357
+ }
358
+ }
359
+
360
+ // ============================================================================
361
+ // INITIALIZATION
362
+ // ============================================================================
246
363
 
364
+ document.addEventListener("DOMContentLoaded", function() {
365
+ const mode = getMode();
366
+ const batch = getBatch();
247
367
 
368
+ // Set up mode/batch toggle listeners
369
+ document.querySelectorAll(".mode-toggle").forEach(btn => {
370
+ btn.addEventListener("click", () => setMode(btn.dataset.mode));
371
+ });
372
+
373
+ document.querySelectorAll(".batch-toggle").forEach(btn => {
374
+ btn.addEventListener("click", () => setBatch(btn.dataset.batch));
375
+ });
248
376
 
377
+ // Restore UI state (without triggering updates)
378
+ setMode(mode, false);
379
+ setBatch(batch, false);
249
380
 
381
+ // Initialize code overlay
382
+ initializeCodeOverlay();
250
383
 
384
+ });
@@ -106,6 +106,7 @@ function initializeCanvas() {
106
106
  document.activeElement?.blur();
107
107
  triggerModal(formHtml, actionName, actionId, state.dropTargetId);
108
108
  });
109
+ initializeCodeOverlay();
109
110
  }
110
111
 
111
112
  function insertDropPlaceholder($target) {
@@ -134,7 +134,7 @@ def parse_optimization_form(form_data: Dict[str, str]):
134
134
  Parse dynamic form data into structured optimization configuration.
135
135
 
136
136
  Expected form field patterns:
137
- - Objectives: {name}_min, {name}_weight
137
+ - Objectives: {name}_obj_min, {name}_weight
138
138
  - Parameters: {name}_type, {name}_min, {name}_max, {name}_choices, {name}_value_type
139
139
  - Config: step{n}_model, step{n}_num_samples
140
140
  """
@@ -149,25 +149,25 @@ def parse_optimization_form(form_data: Dict[str, str]):
149
149
 
150
150
  # Parse objectives
151
151
  for field_name, value in form_data.items():
152
- if field_name.endswith('_min') and value:
152
+ if field_name.endswith('_obj_min') and value:
153
153
  # Extract objective name
154
- obj_name = field_name.replace('_min', '')
154
+ obj_name = field_name.replace('_obj_min', '')
155
155
  if obj_name in processed_objectives:
156
156
  continue
157
157
 
158
158
  # Check if corresponding weight exists
159
159
  weight_field = f"{obj_name}_weight"
160
- if weight_field in form_data and form_data[weight_field]:
161
- objectives.append({
162
- "name": obj_name,
163
- "minimize": value == "minimize",
164
- "weight": float(form_data[weight_field])
165
- })
166
- else:
167
- objectives.append({
160
+ early_stop_field = f"{obj_name}_obj_threshold"
161
+
162
+ config = {
168
163
  "name": obj_name,
169
164
  "minimize": value == "minimize",
170
- })
165
+ }
166
+ if weight_field in form_data and form_data[weight_field]:
167
+ config["weight"] = float(form_data[weight_field])
168
+ if early_stop_field in form_data and form_data[early_stop_field]:
169
+ config["early_stop"] = float(form_data[early_stop_field])
170
+ objectives.append(config)
171
171
  processed_objectives.add(obj_name)
172
172
 
173
173
  # Parse parameters
@@ -192,11 +192,11 @@ def parse_optimization_form(form_data: Dict[str, str]):
192
192
  if value == "range":
193
193
  min_field = f"{param_name}_min"
194
194
  max_field = f"{param_name}_max"
195
-
195
+ step_field = f"{param_name}_step"
196
196
  if min_field in form_data and max_field in form_data:
197
197
  min_val = form_data[min_field]
198
198
  max_val = form_data[max_field]
199
-
199
+ step_val = form_data[step_field] if step_field in form_data else None
200
200
  if min_val and max_val:
201
201
  # Convert based on value_type
202
202
  if value_type == "int":
@@ -204,8 +204,9 @@ def parse_optimization_form(form_data: Dict[str, str]):
204
204
  elif value_type == "float":
205
205
  bounds = [float(min_val), float(max_val)]
206
206
  else: # string
207
- bounds = [str(min_val), str(max_val)]
208
-
207
+ bounds = [float(min_val), float(max_val)]
208
+ if step_val:
209
+ bounds.append(float(step_val))
209
210
  parameter["bounds"] = bounds
210
211
 
211
212
  elif value == "choice":