ivoryos 0.1.9__py3-none-any.whl → 0.1.10__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 (37) hide show
  1. ivoryos/__init__.py +118 -99
  2. ivoryos/config.py +47 -47
  3. ivoryos/routes/auth/auth.py +100 -65
  4. ivoryos/routes/auth/templates/auth/login.html +25 -25
  5. ivoryos/routes/auth/templates/auth/signup.html +32 -32
  6. ivoryos/routes/control/control.py +400 -272
  7. ivoryos/routes/control/templates/control/controllers.html +75 -75
  8. ivoryos/routes/control/templates/control/controllers_home.html +50 -50
  9. ivoryos/routes/control/templates/control/controllers_new.html +89 -89
  10. ivoryos/routes/database/database.py +188 -114
  11. ivoryos/routes/database/templates/database/experiment_database.html +72 -72
  12. ivoryos/routes/design/design.py +541 -416
  13. ivoryos/routes/design/templates/design/experiment_builder.html +415 -415
  14. ivoryos/routes/design/templates/design/experiment_run.html +325 -325
  15. ivoryos/routes/main/main.py +42 -25
  16. ivoryos/routes/main/templates/main/help.html +141 -141
  17. ivoryos/routes/main/templates/main/home.html +68 -68
  18. ivoryos/static/.DS_Store +0 -0
  19. ivoryos/static/js/overlay.js +12 -12
  20. ivoryos/static/js/socket_handler.js +34 -34
  21. ivoryos/static/js/sortable_card.js +24 -24
  22. ivoryos/static/js/sortable_design.js +36 -36
  23. ivoryos/static/style.css +201 -201
  24. ivoryos/templates/base.html +143 -143
  25. ivoryos/utils/db_models.py +518 -518
  26. ivoryos/utils/form.py +316 -316
  27. ivoryos/utils/global_config.py +67 -67
  28. ivoryos/utils/llm_agent.py +183 -183
  29. ivoryos/utils/script_runner.py +165 -164
  30. ivoryos/utils/utils.py +425 -422
  31. ivoryos/version.py +1 -0
  32. {ivoryos-0.1.9.dist-info → ivoryos-0.1.10.dist-info}/LICENSE +21 -21
  33. {ivoryos-0.1.9.dist-info → ivoryos-0.1.10.dist-info}/METADATA +170 -169
  34. ivoryos-0.1.10.dist-info/RECORD +47 -0
  35. {ivoryos-0.1.9.dist-info → ivoryos-0.1.10.dist-info}/WHEEL +1 -1
  36. ivoryos-0.1.9.dist-info/RECORD +0 -45
  37. {ivoryos-0.1.9.dist-info → ivoryos-0.1.10.dist-info}/top_level.txt +0 -0
@@ -1,417 +1,542 @@
1
- import csv
2
- import json
3
- import os
4
- import pickle
5
- import sys
6
-
7
- from flask import Blueprint, redirect, url_for, flash, jsonify, send_file, request, render_template, session, \
8
- current_app, g
9
- from flask_login import login_required
10
- from flask_socketio import SocketIO
11
- from werkzeug.utils import secure_filename
12
-
13
- from ivoryos.utils import utils
14
- from ivoryos.utils.global_config import GlobalConfig
15
- from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo
16
-
17
- from ivoryos.utils.db_models import Script
18
- from ivoryos.utils.script_runner import ScriptRunner
19
-
20
- socketio = SocketIO()
21
- design = Blueprint('design', __name__, template_folder='templates/design')
22
-
23
- global_config = GlobalConfig()
24
- runner = ScriptRunner()
25
-
26
-
27
- @socketio.on('abort_action')
28
- def handle_abort_action():
29
- runner.stop_execution()
30
- socketio.emit('log', {'message': "aborted pending tasks"})
31
-
32
-
33
- @socketio.on('connect')
34
- def handle_abort_action():
35
- # Fetch log messages from local file
36
- filename = os.path.join(current_app.config["OUTPUT_FOLDER"], current_app.config["LOGGERS_PATH"])
37
- with open(filename, 'r') as log_file:
38
- log_history = log_file.readlines()
39
- for message in log_history[-10:]:
40
- socketio.emit('log', {'message': message})
41
-
42
-
43
- @design.route("/experiment/build/", methods=['GET', 'POST'])
44
- @design.route("/experiment/build/<instrument>/", methods=['GET', 'POST'])
45
- @login_required
46
- def experiment_builder(instrument=None):
47
- # global deck
48
- deck = global_config.deck
49
- script = utils.get_script_file()
50
- if deck and script.deck is None:
51
- script.deck = os.path.splitext(os.path.basename(deck.__file__))[
52
- 0] if deck.__name__ == "__main__" else deck.__name__
53
- script.sort_actions()
54
-
55
- pseudo_deck_name = session.get('pseudo_deck', '')
56
- pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
57
- off_line = current_app.config["OFF_LINE"]
58
- enable_llm = current_app.config["ENABLE_LLM"]
59
- autofill = session.get('autofill')
60
-
61
- # autofill is not allowed for prep and cleanup
62
- autofill = autofill if script.editing_type == "script" else False
63
- forms = None
64
- pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
65
- if off_line and pseudo_deck is None:
66
- flash("Choose available deck below.")
67
-
68
- deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
69
-
70
- functions = []
71
- if deck:
72
- deck_variables = global_config.deck_snapshot.keys()
73
- else:
74
- deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
75
- deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
76
- if instrument:
77
- if instrument in ['if', 'while', 'variable', 'wait']:
78
- forms = create_builtin_form(instrument)
79
- else:
80
- if deck:
81
- function_metadata = global_config.deck_snapshot.get(instrument, {})
82
- elif pseudo_deck:
83
- function_metadata = pseudo_deck.get(instrument, {})
84
- functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
85
- forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
86
- if request.method == 'POST' and "hidden_name" in request.form:
87
- all_kwargs = request.form.copy()
88
- method_name = all_kwargs.pop("hidden_name", None)
89
- # if method_name is not None:
90
- form = forms.get(method_name)
91
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
92
-
93
- if form and form.validate_on_submit():
94
- function_name = kwargs.pop("hidden_name")
95
- save_data = kwargs.pop('return', '')
96
- variable_kwargs = {}
97
- variable_kwargs_types = {}
98
-
99
- try:
100
- variable_kwargs, variable_kwargs_types = utils.find_variable_in_script(script, kwargs)
101
-
102
- for name in variable_kwargs.keys():
103
- del kwargs[name]
104
- primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
105
-
106
- except:
107
- primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
108
-
109
- kwargs.update(variable_kwargs)
110
- arg_types = {}
111
- arg_types.update(variable_kwargs_types)
112
- arg_types.update(primitive_arg_types)
113
- all_kwargs.update(variable_kwargs)
114
-
115
- action = {"instrument": instrument, "action": function_name,
116
- "args": {name: arg for (name, arg) in kwargs.items()},
117
- "return": save_data,
118
- 'arg_types': arg_types}
119
- script.add_action(action=action)
120
- else:
121
- flash(form.errors)
122
-
123
- elif request.method == 'POST' and "builtin_name" in request.form:
124
- kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
125
- if forms.validate_on_submit():
126
- logic_type = kwargs.pop('builtin_name')
127
- if 'variable' in kwargs:
128
- script.add_variable(**kwargs)
129
- else:
130
- script.add_logic_action(logic_type=logic_type, **kwargs)
131
- else:
132
- flash(forms.errors)
133
-
134
- # toggle autofill
135
- elif request.method == 'POST' and "autofill" in request.form:
136
- autofill = not autofill
137
- forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
138
- session['autofill'] = autofill
139
- utils.post_script_file(script)
140
- design_buttons = [create_action_button(i) for i in script.currently_editing_script]
141
- return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
142
- script=script, defined_variables=deck_variables,
143
- local_variables=global_config.defined_variables,
144
- functions=functions, forms=forms, buttons=design_buttons, format_name=format_name,
145
- use_llm=enable_llm)
146
-
147
-
148
- @design.route("/generate_code", methods=['POST'])
149
- @login_required
150
- def generate_code():
151
- agent = global_config.agent
152
- enable_llm = current_app.config["ENABLE_LLM"]
153
- instrument = request.form.get("instrument")
154
-
155
- if request.method == 'POST' and "clear" in request.form:
156
- session['prompt'][instrument] = ''
157
- if request.method == 'POST' and "gen" in request.form:
158
- prompt = request.form.get("prompt")
159
- session['prompt'][instrument] = prompt
160
- # sdl_module = utils.parse_functions(find_instrument_by_name(f'deck.{instrument}'), doc_string=True)
161
- sdl_module = global_config.deck_snapshot.get(instrument, {})
162
- empty_script = Script(author=session.get('user'))
163
- if enable_llm and agent is None:
164
- try:
165
- model = current_app.config["LLM_MODEL"]
166
- server = current_app.config["LLM_SERVER"]
167
- module = current_app.config["MODULE"]
168
- from ivoryos.utils.llm_agent import LlmAgent
169
- agent = LlmAgent(host=server, model=model, output_path=os.path.dirname(os.path.abspath(module)))
170
- except Exception as e:
171
- flash(e.__str__())
172
- action_list = agent.generate_code(sdl_module, prompt)
173
- for action in action_list:
174
- action['instrument'] = instrument
175
- action['return'] = ''
176
- if "args" not in action:
177
- action['args'] = {}
178
- if "arg_types" not in action:
179
- action['arg_types'] = {}
180
- empty_script.add_action(action)
181
- utils.post_script_file(empty_script)
182
- return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True))
183
-
184
-
185
- @design.route("/experiment", methods=['GET', 'POST'])
186
- @login_required
187
- def experiment_run():
188
- # global deck
189
- deck = global_config.deck
190
- script = utils.get_script_file()
191
- script.sort_actions()
192
- off_line = current_app.config["OFF_LINE"]
193
- deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
194
- # if not off_line and deck is None:
195
- # # print("loading deck")
196
- # module = current_app.config.get('MODULE', '')
197
- # deck = sys.modules[module] if module else None
198
- # script.deck = os.path.splitext(os.path.basename(deck.__file__))[0]
199
- design_buttons = {stype: [create_action_button(i) for i in script.get_script(stype)] for stype in script.stypes}
200
- config_preview = []
201
- config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
202
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
203
- # print(exec_string)
204
- config_file = request.args.get("filename")
205
- config = []
206
- if config_file:
207
- session['config_file'] = config_file
208
- filename = session.get("config_file")
209
- if filename:
210
- # config_preview = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
211
- config = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
212
- config_preview = config[1:6]
213
- arg_type = config.pop(0) # first entry is types
214
- try:
215
- exec(exec_string)
216
- except Exception:
217
- flash("Please check syntax!!")
218
- return redirect(url_for("design.experiment_builder"))
219
- # runner.globals_dict.update(globals())
220
- run_name = script.name if script.name else "untitled"
221
-
222
- dismiss = session.get("dismiss", None)
223
- script = utils.get_script_file()
224
- no_deck_warning = False
225
-
226
- _, return_list = script.config_return()
227
- config_list, config_type_list = script.config("script")
228
- # config = script.config("script")
229
- data_list = os.listdir(current_app.config['DATA_FOLDER'])
230
- data_list.remove(".gitkeep") if ".gitkeep" in data_list else data_list
231
- if deck is None:
232
- no_deck_warning = True
233
- flash(f"No deck is found, import {script.deck}")
234
- elif script.deck:
235
- is_deck_match = script.deck == deck.__name__ or script.deck == \
236
- os.path.splitext(os.path.basename(deck.__file__))[0]
237
- if not is_deck_match:
238
- flash(f"This script is not compatible with current deck, import {script.deck}")
239
- if request.method == "POST":
240
- bo_args = None
241
- if "bo" in request.form:
242
- bo_args = request.form.to_dict()
243
- # ax_client = utils.ax_initiation(bo_args)
244
- if "online-config" in request.form:
245
- config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
246
- repeat = request.form.get('repeat', None)
247
-
248
- try:
249
- datapath = current_app.config["DATA_FOLDER"]
250
- run_name = script.validate_function_name(run_name)
251
- runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
252
- logger=g.logger, socketio=g.socketio, repeat_count=repeat,
253
- output_path=datapath
254
- )
255
- except Exception as e:
256
- flash(e)
257
- return render_template('experiment_run.html', script=script.script_dict, filename=filename, dot_py=exec_string,
258
- return_list=return_list, config_list=config_list, config_file_list=config_file_list,
259
- config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
260
- no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons, history=deck_list)
261
-
262
-
263
- @design.route("/toggle_script_type/<stype>")
264
- @login_required
265
- def toggle_script_type(stype=None):
266
- script = utils.get_script_file()
267
- script.editing_type = stype
268
- utils.post_script_file(script)
269
- return redirect(url_for('design.experiment_builder'))
270
-
271
-
272
- @design.route("/updateList", methods=['GET', 'POST'])
273
- @login_required
274
- def update_list():
275
- order = request.form['order']
276
- script = utils.get_script_file()
277
- script.currently_editing_order = order.split(",", len(script.currently_editing_script))
278
- utils.post_script_file(script)
279
- return jsonify('Successfully Updated')
280
-
281
-
282
- # --------------------handle all the import/export and download/upload--------------------------
283
- @design.route("/clear")
284
- @login_required
285
- def clear():
286
- deck = global_config.deck
287
- pseudo_name = session.get("pseudo_deck", "")
288
- if deck:
289
- deck_name = os.path.splitext(os.path.basename(deck.__file__))[
290
- 0] if deck.__name__ == "__main__" else deck.__name__
291
- elif pseudo_name:
292
- deck_name = pseudo_name
293
- else:
294
- deck_name = ''
295
- script = Script(deck=deck_name, author=session.get('username'))
296
- utils.post_script_file(script)
297
- return redirect(url_for("design.experiment_builder"))
298
-
299
-
300
- @design.route("/import_pseudo", methods=['GET', 'POST'])
301
- @login_required
302
- def import_pseudo():
303
- pkl_name = request.form.get('pkl_name')
304
- script = utils.get_script_file()
305
- session['pseudo_deck'] = pkl_name
306
-
307
- if script.deck is None or script.isEmpty():
308
- script.deck = pkl_name.split('.')[0]
309
- utils.post_script_file(script)
310
- elif script.deck and not script.deck == pkl_name.split('.')[0]:
311
- flash(f"Choose the deck with name {script.deck}")
312
- return redirect(url_for("design.experiment_builder"))
313
-
314
-
315
- @design.route('/uploads', methods=['GET', 'POST'])
316
- @login_required
317
- def upload():
318
- """
319
- upload csv configuration file
320
- :return:
321
- """
322
- if request.method == "POST":
323
- f = request.files['file']
324
- if 'file' not in request.files:
325
- flash('No file part')
326
- if f.filename.split('.')[-1] == "csv":
327
- filename = secure_filename(f.filename)
328
- f.save(os.path.join(current_app.config['CSV_FOLDER'], filename))
329
- session['config_file'] = filename
330
- return redirect(url_for("design.experiment_run"))
331
- else:
332
- flash("Config file is in csv format")
333
- return redirect(url_for("design.experiment_run"))
334
-
335
-
336
- @design.route('/download_results/<filename>')
337
- @login_required
338
- def download_results(filename):
339
- filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
340
- return send_file(os.path.abspath(filepath), as_attachment=True)
341
-
342
-
343
- @design.route('/load_json', methods=['GET', 'POST'])
344
- @login_required
345
- def load_json():
346
- if request.method == "POST":
347
- f = request.files['file']
348
- if 'file' not in request.files:
349
- flash('No file part')
350
- if f.filename.endswith("json"):
351
- script_dict = json.load(f)
352
- utils.post_script_file(script_dict, is_dict=True)
353
- else:
354
- flash("Script file need to be JSON file")
355
- return redirect(url_for("design.experiment_builder"))
356
-
357
-
358
- @design.route('/download/<filetype>')
359
- @login_required
360
- def download(filetype):
361
- script = utils.get_script_file()
362
- run_name = script.name if script.name else "untitled"
363
- if filetype == "configure":
364
- filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
365
- with open(filepath, 'w', newline='') as f:
366
- writer = csv.writer(f)
367
- cfg, cfg_types = script.config("script")
368
- writer.writerow(cfg)
369
- writer.writerow(list(cfg_types.values()))
370
- elif filetype == "script":
371
- script.sort_actions()
372
- json_object = json.dumps(script.as_dict())
373
- filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}.json")
374
- with open(filepath, "w") as outfile:
375
- outfile.write(json_object)
376
- elif filetype == "python":
377
- filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
378
-
379
- return send_file(os.path.abspath(filepath), as_attachment=True)
380
-
381
-
382
- @design.route("/edit/<uuid>", methods=['GET', 'POST'])
383
- @login_required
384
- def edit_action(uuid):
385
- script = utils.get_script_file()
386
- action = script.find_by_uuid(uuid)
387
- session['edit_action'] = action
388
- if request.method == "POST":
389
- if "back" not in request.form:
390
- args = request.form.to_dict()
391
- save_as = args.pop('return', '')
392
- try:
393
- script.update_by_uuid(uuid=uuid, args=args, output=save_as)
394
- except Exception as e:
395
- flash(e.__str__())
396
- session.pop('edit_action')
397
- return redirect(url_for('design.experiment_builder'))
398
-
399
-
400
- @design.route("/delete/<id>", methods=['GET', 'POST'])
401
- @login_required
402
- def delete_action(id):
403
- back = request.referrer
404
- script = utils.get_script_file()
405
- script.delete_action(id)
406
- utils.post_script_file(script)
407
- return redirect(back)
408
-
409
-
410
- @design.route("/duplicate/<id>", methods=['GET', 'POST'])
411
- @login_required
412
- def duplicate_action(id):
413
- back = request.referrer
414
- script = utils.get_script_file()
415
- script.duplicate_action(id)
416
- utils.post_script_file(script)
1
+ import csv
2
+ import json
3
+ import os
4
+ import pickle
5
+ import sys
6
+
7
+ from flask import Blueprint, redirect, url_for, flash, jsonify, send_file, request, render_template, session, \
8
+ current_app, g
9
+ from flask_login import login_required
10
+ from flask_socketio import SocketIO
11
+ from werkzeug.utils import secure_filename
12
+
13
+ from ivoryos.utils import utils
14
+ from ivoryos.utils.global_config import GlobalConfig
15
+ from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo
16
+ from ivoryos.utils.db_models import Script
17
+ from ivoryos.utils.script_runner import ScriptRunner
18
+
19
+ socketio = SocketIO()
20
+ design = Blueprint('design', __name__, template_folder='templates/design')
21
+
22
+ global_config = GlobalConfig()
23
+ runner = ScriptRunner()
24
+
25
+
26
+ @socketio.on('abort_action')
27
+ def handle_abort_action():
28
+ runner.stop_execution()
29
+ socketio.emit('log', {'message': "aborted pending tasks"})
30
+
31
+
32
+ @socketio.on('connect')
33
+ def handle_abort_action():
34
+ # Fetch log messages from local file
35
+ filename = os.path.join(current_app.config["OUTPUT_FOLDER"], current_app.config["LOGGERS_PATH"])
36
+ with open(filename, 'r') as log_file:
37
+ log_history = log_file.readlines()
38
+ for message in log_history[-10:]:
39
+ socketio.emit('log', {'message': message})
40
+
41
+
42
+ @design.route("/experiment/build/", methods=['GET', 'POST'])
43
+ @design.route("/experiment/build/<instrument>/", methods=['GET', 'POST'])
44
+ @login_required
45
+ def experiment_builder(instrument=None):
46
+ """
47
+ .. :quickref: Workflow Design; Build experiment workflow
48
+
49
+ **Experiment Builder**
50
+
51
+ This route allows users to build and edit experiment workflows. Users can interact with available instruments,
52
+ define variables, and manage experiment scripts.
53
+
54
+ .. http:get:: /experiment/build
55
+
56
+ Load the experiment builder interface.
57
+
58
+ :param instrument: The specific instrument for which to load functions and forms.
59
+ :type instrument: str
60
+ :status 200: Experiment builder loaded successfully.
61
+
62
+ .. http:post:: /experiment/build
63
+
64
+ Submit form data to add or modify actions in the experiment script.
65
+
66
+ **Adding action to canvas**
67
+
68
+ :form return: (optional) The name of the function or method to add to the script.
69
+ :form dynamic: depend on the selected instrument and its metadata.
70
+
71
+ :status 200: Action added or modified successfully.
72
+ :status 400: Validation errors in submitted form data.
73
+ :status 302: Toggles autofill or redirects to refresh the page.
74
+
75
+ **Toggle auto parameter name fill**:
76
+
77
+ :status 200: autofill toggled successfully
78
+
79
+ """
80
+ deck = global_config.deck
81
+ script = utils.get_script_file()
82
+ if deck and script.deck is None:
83
+ script.deck = os.path.splitext(os.path.basename(deck.__file__))[
84
+ 0] if deck.__name__ == "__main__" else deck.__name__
85
+ script.sort_actions()
86
+
87
+ pseudo_deck_name = session.get('pseudo_deck', '')
88
+ pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
89
+ off_line = current_app.config["OFF_LINE"]
90
+ enable_llm = current_app.config["ENABLE_LLM"]
91
+ autofill = session.get('autofill')
92
+
93
+ # autofill is not allowed for prep and cleanup
94
+ autofill = autofill if script.editing_type == "script" else False
95
+ forms = None
96
+ pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
97
+ if off_line and pseudo_deck is None:
98
+ flash("Choose available deck below.")
99
+
100
+ deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
101
+
102
+ functions = []
103
+ if deck:
104
+ deck_variables = global_config.deck_snapshot.keys()
105
+ else:
106
+ deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
107
+ deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
108
+ if instrument:
109
+ if instrument in ['if', 'while', 'variable', 'wait']:
110
+ forms = create_builtin_form(instrument)
111
+ else:
112
+ if deck:
113
+ function_metadata = global_config.deck_snapshot.get(instrument, {})
114
+ elif pseudo_deck:
115
+ function_metadata = pseudo_deck.get(instrument, {})
116
+ functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
117
+ forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
118
+ if request.method == 'POST' and "hidden_name" in request.form:
119
+ all_kwargs = request.form.copy()
120
+ method_name = all_kwargs.pop("hidden_name", None)
121
+ # if method_name is not None:
122
+ form = forms.get(method_name)
123
+ kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
124
+
125
+ if form and form.validate_on_submit():
126
+ function_name = kwargs.pop("hidden_name")
127
+ save_data = kwargs.pop('return', '')
128
+ variable_kwargs = {}
129
+ variable_kwargs_types = {}
130
+
131
+ try:
132
+ variable_kwargs, variable_kwargs_types = utils.find_variable_in_script(script, kwargs)
133
+
134
+ for name in variable_kwargs.keys():
135
+ del kwargs[name]
136
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
137
+
138
+ except:
139
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
140
+
141
+ kwargs.update(variable_kwargs)
142
+ arg_types = {}
143
+ arg_types.update(variable_kwargs_types)
144
+ arg_types.update(primitive_arg_types)
145
+ all_kwargs.update(variable_kwargs)
146
+
147
+ action = {"instrument": instrument, "action": function_name,
148
+ "args": {name: arg for (name, arg) in kwargs.items()},
149
+ "return": save_data,
150
+ 'arg_types': arg_types}
151
+ script.add_action(action=action)
152
+ else:
153
+ flash(form.errors)
154
+
155
+ elif request.method == 'POST' and "builtin_name" in request.form:
156
+ kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
157
+ if forms.validate_on_submit():
158
+ logic_type = kwargs.pop('builtin_name')
159
+ if 'variable' in kwargs:
160
+ script.add_variable(**kwargs)
161
+ else:
162
+ script.add_logic_action(logic_type=logic_type, **kwargs)
163
+ else:
164
+ flash(forms.errors)
165
+
166
+ # toggle autofill
167
+ elif request.method == 'POST' and "autofill" in request.form:
168
+ autofill = not autofill
169
+ forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
170
+ session['autofill'] = autofill
171
+ utils.post_script_file(script)
172
+ design_buttons = [create_action_button(i) for i in script.currently_editing_script]
173
+ return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
174
+ script=script, defined_variables=deck_variables,
175
+ local_variables=global_config.defined_variables,
176
+ functions=functions, forms=forms, buttons=design_buttons, format_name=format_name,
177
+ use_llm=enable_llm)
178
+
179
+
180
+ @design.route("/generate_code", methods=['POST'])
181
+ @login_required
182
+ def generate_code():
183
+ """
184
+ .. :quickref: Text to Code; Generate code from user input and update the design canvas.
185
+
186
+ .. http:post:: /generate_code
187
+
188
+ :form prompt: user's prompt
189
+ :status 200: and then redirects to :http:get:`/experiment/build`
190
+ :status 400: failed to initialize the AI agent redirects to :http:get:`/experiment/build`
191
+
192
+ """
193
+ agent = global_config.agent
194
+ enable_llm = current_app.config["ENABLE_LLM"]
195
+ instrument = request.form.get("instrument")
196
+
197
+ if request.method == 'POST' and "clear" in request.form:
198
+ session['prompt'][instrument] = ''
199
+ if request.method == 'POST' and "gen" in request.form:
200
+ prompt = request.form.get("prompt")
201
+ session['prompt'][instrument] = prompt
202
+ # sdl_module = utils.parse_functions(find_instrument_by_name(f'deck.{instrument}'), doc_string=True)
203
+ sdl_module = global_config.deck_snapshot.get(instrument, {})
204
+ empty_script = Script(author=session.get('user'))
205
+ if enable_llm and agent is None:
206
+ try:
207
+ model = current_app.config["LLM_MODEL"]
208
+ server = current_app.config["LLM_SERVER"]
209
+ module = current_app.config["MODULE"]
210
+ from ivoryos.utils.llm_agent import LlmAgent
211
+ agent = LlmAgent(host=server, model=model, output_path=os.path.dirname(os.path.abspath(module)))
212
+ except Exception as e:
213
+ flash(e.__str__())
214
+ return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True)), 400
215
+ action_list = agent.generate_code(sdl_module, prompt)
216
+ for action in action_list:
217
+ action['instrument'] = instrument
218
+ action['return'] = ''
219
+ if "args" not in action:
220
+ action['args'] = {}
221
+ if "arg_types" not in action:
222
+ action['arg_types'] = {}
223
+ empty_script.add_action(action)
224
+ utils.post_script_file(empty_script)
225
+ return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True))
226
+
227
+
228
+ @design.route("/experiment", methods=['GET', 'POST'])
229
+ @login_required
230
+ def experiment_run():
231
+ """
232
+ .. :quickref: Workflow Execution; Execute/iterate the workflow
233
+
234
+ .. http:get:: /experiment
235
+
236
+ Compile the workflow and load the experiment execution interface.
237
+
238
+ .. http:post:: /experiment
239
+
240
+ Start workflow execution
241
+
242
+ """
243
+ deck = global_config.deck
244
+ script = utils.get_script_file()
245
+ script.sort_actions()
246
+ off_line = current_app.config["OFF_LINE"]
247
+ deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
248
+ # if not off_line and deck is None:
249
+ # # print("loading deck")
250
+ # module = current_app.config.get('MODULE', '')
251
+ # deck = sys.modules[module] if module else None
252
+ # script.deck = os.path.splitext(os.path.basename(deck.__file__))[0]
253
+ design_buttons = {stype: [create_action_button(i) for i in script.get_script(stype)] for stype in script.stypes}
254
+ config_preview = []
255
+ config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
256
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
257
+ # print(exec_string)
258
+ config_file = request.args.get("filename")
259
+ config = []
260
+ if config_file:
261
+ session['config_file'] = config_file
262
+ filename = session.get("config_file")
263
+ if filename:
264
+ # config_preview = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
265
+ config = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
266
+ config_preview = config[1:6]
267
+ arg_type = config.pop(0) # first entry is types
268
+ try:
269
+ exec(exec_string)
270
+ except Exception:
271
+ flash("Please check syntax!!")
272
+ return redirect(url_for("design.experiment_builder"))
273
+ # runner.globals_dict.update(globals())
274
+ run_name = script.name if script.name else "untitled"
275
+
276
+ dismiss = session.get("dismiss", None)
277
+ script = utils.get_script_file()
278
+ no_deck_warning = False
279
+
280
+ _, return_list = script.config_return()
281
+ config_list, config_type_list = script.config("script")
282
+ # config = script.config("script")
283
+ data_list = os.listdir(current_app.config['DATA_FOLDER'])
284
+ data_list.remove(".gitkeep") if ".gitkeep" in data_list else data_list
285
+ if deck is None:
286
+ no_deck_warning = True
287
+ flash(f"No deck is found, import {script.deck}")
288
+ elif script.deck:
289
+ is_deck_match = script.deck == deck.__name__ or script.deck == \
290
+ os.path.splitext(os.path.basename(deck.__file__))[0]
291
+ if not is_deck_match:
292
+ flash(f"This script is not compatible with current deck, import {script.deck}")
293
+ if request.method == "POST":
294
+ bo_args = None
295
+ if "bo" in request.form:
296
+ bo_args = request.form.to_dict()
297
+ # ax_client = utils.ax_initiation(bo_args)
298
+ if "online-config" in request.form:
299
+ config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
300
+ repeat = request.form.get('repeat', None)
301
+
302
+ try:
303
+ datapath = current_app.config["DATA_FOLDER"]
304
+ run_name = script.validate_function_name(run_name)
305
+ runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
306
+ logger=g.logger, socketio=g.socketio, repeat_count=repeat,
307
+ output_path=datapath
308
+ )
309
+ except Exception as e:
310
+ flash(e)
311
+ return render_template('experiment_run.html', script=script.script_dict, filename=filename, dot_py=exec_string,
312
+ return_list=return_list, config_list=config_list, config_file_list=config_file_list,
313
+ config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
314
+ no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons, history=deck_list)
315
+
316
+
317
+ @design.route("/toggle_script_type/<stype>")
318
+ @login_required
319
+ def toggle_script_type(stype=None):
320
+ """
321
+ .. :quickref: Workflow Design; toggle the experimental phase for design canvas.
322
+
323
+ .. http:get:: /toggle_script_type
324
+
325
+ :status 200: and then redirects to :http:get:`/experiment/build`
326
+
327
+ """
328
+ script = utils.get_script_file()
329
+ script.editing_type = stype
330
+ utils.post_script_file(script)
331
+ return redirect(url_for('design.experiment_builder'))
332
+
333
+
334
+ @design.route("/updateList", methods=['GET', 'POST'])
335
+ @login_required
336
+ def update_list():
337
+ order = request.form['order']
338
+ script = utils.get_script_file()
339
+ script.currently_editing_order = order.split(",", len(script.currently_editing_script))
340
+ utils.post_script_file(script)
341
+ return jsonify('Successfully Updated')
342
+
343
+
344
+ # --------------------handle all the import/export and download/upload--------------------------
345
+ @design.route("/clear")
346
+ @login_required
347
+ def clear():
348
+ """
349
+ .. :quickref: Workflow Design; clear the design canvas.
350
+
351
+ .. http:get:: /clear
352
+
353
+ :form prompt: user's prompt
354
+ :status 200: clear canvas and then redirects to :http:get:`/experiment/build`
355
+ """
356
+ deck = global_config.deck
357
+ pseudo_name = session.get("pseudo_deck", "")
358
+ if deck:
359
+ deck_name = os.path.splitext(os.path.basename(deck.__file__))[
360
+ 0] if deck.__name__ == "__main__" else deck.__name__
361
+ elif pseudo_name:
362
+ deck_name = pseudo_name
363
+ else:
364
+ deck_name = ''
365
+ script = Script(deck=deck_name, author=session.get('username'))
366
+ utils.post_script_file(script)
367
+ return redirect(url_for("design.experiment_builder"))
368
+
369
+
370
+ @design.route("/import_pseudo", methods=['POST'])
371
+ @login_required
372
+ def import_pseudo():
373
+ """
374
+ .. :quickref: Workflow Design; Import pseudo deck from deck history
375
+
376
+ .. http:post:: /import_pseudo
377
+
378
+ :form pkl_name: pseudo deck name
379
+ :status 302: load pseudo deck and then redirects to :http:get:`/experiment/build`
380
+ """
381
+ pkl_name = request.form.get('pkl_name')
382
+ script = utils.get_script_file()
383
+ session['pseudo_deck'] = pkl_name
384
+
385
+ if script.deck is None or script.isEmpty():
386
+ script.deck = pkl_name.split('.')[0]
387
+ utils.post_script_file(script)
388
+ elif script.deck and not script.deck == pkl_name.split('.')[0]:
389
+ flash(f"Choose the deck with name {script.deck}")
390
+ return redirect(url_for("design.experiment_builder"))
391
+
392
+
393
+ @design.route('/uploads', methods=['POST'])
394
+ @login_required
395
+ def upload():
396
+ """
397
+ .. :quickref: Workflow Execution; upload a workflow config file (.CSV)
398
+
399
+ .. http:post:: /uploads
400
+
401
+ :form file: workflow CSV config file
402
+ :status 302: save csv file and then redirects to :http:get:`/experiment`
403
+ """
404
+ if request.method == "POST":
405
+ f = request.files['file']
406
+ if 'file' not in request.files:
407
+ flash('No file part')
408
+ if f.filename.split('.')[-1] == "csv":
409
+ filename = secure_filename(f.filename)
410
+ f.save(os.path.join(current_app.config['CSV_FOLDER'], filename))
411
+ session['config_file'] = filename
412
+ return redirect(url_for("design.experiment_run"))
413
+ else:
414
+ flash("Config file is in csv format")
415
+ return redirect(url_for("design.experiment_run"))
416
+
417
+
418
+ @design.route('/download_results/<filename>')
419
+ @login_required
420
+ def download_results(filename):
421
+ filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
422
+ return send_file(os.path.abspath(filepath), as_attachment=True)
423
+
424
+
425
+ @design.route('/load_json', methods=['POST'])
426
+ @login_required
427
+ def load_json():
428
+ """
429
+ .. :quickref: Workflow Design Ext; upload a workflow design file (.JSON)
430
+
431
+ .. http:post:: /load_json
432
+
433
+ :form file: workflow design JSON file
434
+ :status 302: load pseudo deck and then redirects to :http:get:`/experiment/build`
435
+ """
436
+ if request.method == "POST":
437
+ f = request.files['file']
438
+ if 'file' not in request.files:
439
+ flash('No file part')
440
+ if f.filename.endswith("json"):
441
+ script_dict = json.load(f)
442
+ utils.post_script_file(script_dict, is_dict=True)
443
+ else:
444
+ flash("Script file need to be JSON file")
445
+ return redirect(url_for("design.experiment_builder"))
446
+
447
+
448
+ @design.route('/download/<filetype>')
449
+ @login_required
450
+ def download(filetype):
451
+ script = utils.get_script_file()
452
+ run_name = script.name if script.name else "untitled"
453
+ if filetype == "configure":
454
+ filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
455
+ with open(filepath, 'w', newline='') as f:
456
+ writer = csv.writer(f)
457
+ cfg, cfg_types = script.config("script")
458
+ writer.writerow(cfg)
459
+ writer.writerow(list(cfg_types.values()))
460
+ elif filetype == "script":
461
+ script.sort_actions()
462
+ json_object = json.dumps(script.as_dict())
463
+ filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}.json")
464
+ with open(filepath, "w") as outfile:
465
+ outfile.write(json_object)
466
+ elif filetype == "python":
467
+ filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
468
+
469
+ return send_file(os.path.abspath(filepath), as_attachment=True)
470
+
471
+
472
+ @design.route("/edit/<uuid>", methods=['GET', 'POST'])
473
+ @login_required
474
+ def edit_action(uuid: str):
475
+ """
476
+ .. :quickref: Workflow Design; edit parameters of an action step on canvas
477
+
478
+ .. http:get:: /edit
479
+
480
+ Load parameter form of an action step
481
+
482
+ .. http:post:: /edit
483
+
484
+ :param uuid: The step's uuid
485
+ :type uuid: str
486
+
487
+ :form dynamic form: workflow step dynamic inputs
488
+ :status 302: save changes and then redirects to :http:get:`/experiment/build`
489
+ """
490
+ script = utils.get_script_file()
491
+ action = script.find_by_uuid(uuid)
492
+ session['edit_action'] = action
493
+ if request.method == "POST":
494
+ if "back" not in request.form:
495
+ args = request.form.to_dict()
496
+ save_as = args.pop('return', '')
497
+ try:
498
+ script.update_by_uuid(uuid=uuid, args=args, output=save_as)
499
+ except Exception as e:
500
+ flash(e.__str__())
501
+ session.pop('edit_action')
502
+ return redirect(url_for('design.experiment_builder'))
503
+
504
+
505
+ @design.route("/delete/<id>")
506
+ @login_required
507
+ def delete_action(id: int):
508
+ """
509
+ .. :quickref: Workflow Design; delete an action step on canvas
510
+
511
+ .. http:get:: /delete
512
+
513
+ :param id: The step number id
514
+ :type id: int
515
+
516
+ :status 302: save changes and then redirects to :http:get:`/experiment/build`
517
+ """
518
+ back = request.referrer
519
+ script = utils.get_script_file()
520
+ script.delete_action(id)
521
+ utils.post_script_file(script)
522
+ return redirect(back)
523
+
524
+
525
+ @design.route("/duplicate/<id>")
526
+ @login_required
527
+ def duplicate_action(id: int):
528
+ """
529
+ .. :quickref: Workflow Design; duplicate an action step on canvas
530
+
531
+ .. http:get:: /duplicate
532
+
533
+ :param id: The step number id
534
+ :type id: int
535
+
536
+ :status 302: save changes and then redirects to :http:get:`/experiment/build`
537
+ """
538
+ back = request.referrer
539
+ script = utils.get_script_file()
540
+ script.duplicate_action(id)
541
+ utils.post_script_file(script)
417
542
  return redirect(back)