ivoryos 1.3.2__tar.gz → 1.3.4__tar.gz

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 (109) hide show
  1. {ivoryos-1.3.2 → ivoryos-1.3.4}/PKG-INFO +31 -6
  2. {ivoryos-1.3.2 → ivoryos-1.3.4}/README.md +30 -5
  3. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/app.py +2 -1
  4. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/server.py +29 -22
  5. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/global_config.py +11 -0
  6. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/py_to_json.py +35 -6
  7. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/script_runner.py +13 -5
  8. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/task_runner.py +2 -1
  9. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/utils.py +1 -0
  10. ivoryos-1.3.4/ivoryos/version.py +1 -0
  11. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos.egg-info/PKG-INFO +31 -6
  12. ivoryos-1.3.2/ivoryos/version.py +0 -1
  13. {ivoryos-1.3.2 → ivoryos-1.3.4}/LICENSE +0 -0
  14. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/__init__.py +0 -0
  15. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/config.py +0 -0
  16. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/optimizer/ax_optimizer.py +0 -0
  17. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/optimizer/base_optimizer.py +0 -0
  18. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  19. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/optimizer/registry.py +0 -0
  20. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/__init__.py +0 -0
  21. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/api/api.py +0 -0
  22. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/auth/__init__.py +0 -0
  23. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/auth/auth.py +0 -0
  24. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/auth/templates/login.html +0 -0
  25. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/auth/templates/signup.html +0 -0
  26. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/__init__.py +0 -0
  27. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/control.py +0 -0
  28. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/control_file.py +0 -0
  29. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/control_new_device.py +0 -0
  30. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/templates/controllers.html +0 -0
  31. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  32. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/control/utils.py +0 -0
  33. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/data/__init__.py +0 -0
  34. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/data/data.py +0 -0
  35. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/data/templates/components/step_card.html +0 -0
  36. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  37. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/data/templates/workflow_view.html +0 -0
  38. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/__init__.py +0 -0
  39. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/design.py +0 -0
  40. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/design_file.py +0 -0
  41. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/design_step.py +0 -0
  42. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  43. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  44. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  45. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  46. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  47. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  48. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
  49. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  50. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
  51. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  52. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  53. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  54. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  55. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  56. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  57. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/modals.html +0 -0
  58. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  59. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  60. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  61. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  62. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/__init__.py +0 -0
  63. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/execute.py +0 -0
  64. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/execute_file.py +0 -0
  65. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  66. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  67. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  68. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  69. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  70. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  71. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  72. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  73. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  74. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/library/__init__.py +0 -0
  75. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/library/library.py +0 -0
  76. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/library/templates/library.html +0 -0
  77. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/main/__init__.py +0 -0
  78. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/main/main.py +0 -0
  79. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/main/templates/help.html +0 -0
  80. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/routes/main/templates/home.html +0 -0
  81. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/socket_handlers.py +0 -0
  82. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/favicon.ico +0 -0
  83. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  84. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  85. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/action_handlers.js +0 -0
  86. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/db_delete.js +0 -0
  87. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/overlay.js +0 -0
  88. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/script_metadata.js +0 -0
  89. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/socket_handler.js +0 -0
  90. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/sortable_card.js +0 -0
  91. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/sortable_design.js +0 -0
  92. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/js/ui_state.js +0 -0
  93. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/logo.webp +0 -0
  94. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/static/style.css +0 -0
  95. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/templates/base.html +0 -0
  96. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/__init__.py +0 -0
  97. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/bo_campaign.py +0 -0
  98. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/client_proxy.py +0 -0
  99. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/db_models.py +0 -0
  100. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/decorators.py +0 -0
  101. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/form.py +0 -0
  102. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/llm_agent.py +0 -0
  103. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos/utils/serilize.py +0 -0
  104. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos.egg-info/SOURCES.txt +0 -0
  105. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos.egg-info/dependency_links.txt +0 -0
  106. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos.egg-info/requires.txt +0 -0
  107. {ivoryos-1.3.2 → ivoryos-1.3.4}/ivoryos.egg-info/top_level.txt +0 -0
  108. {ivoryos-1.3.2 → ivoryos-1.3.4}/pyproject.toml +0 -0
  109. {ivoryos-1.3.2 → ivoryos-1.3.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -28,7 +28,8 @@ Dynamic: license-file
28
28
  [![YouTube](https://img.shields.io/badge/YouTube-tutorial-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
29
29
  [![YouTube](https://img.shields.io/badge/YouTube-demo-red?logo=youtube)](https://youtu.be/flr5ydiE96s)
30
30
  [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
31
- [![Discord](https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2)](https://discord.gg/AX5P9EdGVX)
31
+
32
+ [//]: # ([![Discord]&#40;https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2&#41;]&#40;https://discord.gg/AX5P9EdGVX&#41;)
32
33
 
33
34
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
34
35
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -101,11 +102,14 @@ pip install -e .
101
102
  ## Quick start
102
103
  In your SDL script,
103
104
  ```python
105
+ my_robot = Robot()
106
+
104
107
  import ivoryos
105
108
 
106
109
  ivoryos.run(__name__)
107
110
  ```
108
- Login: Create an account (local DB, bcrypt password)
111
+ You can now access the web UI at http://127.0.0.1:8000,
112
+ create an account, login, and start designing workflows!
109
113
 
110
114
  ----
111
115
  ## Features
@@ -127,6 +131,28 @@ Add single or multiple loggers:
127
131
  ivoryos.run(__name__, logger="logger name")
128
132
  ivoryos.run(__name__, logger=["logger 1", "logger 2"])
129
133
  ```
134
+ ### Human-in-the-loop
135
+ Add single or multiple notification handlers for `pause` feature in flow control:
136
+ ```python
137
+
138
+ def slack_bot(msg: str = "Hi"):
139
+ """
140
+ a function that can be used as a notification handler function("msg")
141
+ :param msg: message to send
142
+ """
143
+ from slack_sdk import WebClient
144
+
145
+ slack_token = "your slack token"
146
+ client = WebClient(token=slack_token)
147
+
148
+ my_user_id = "your user id" # replace with your actual Slack user ID
149
+
150
+ client.chat_postMessage(channel=my_user_id, text=msg)
151
+
152
+ import ivoryos
153
+ ivoryos.run(__name__, notification_handler=slack_bot)
154
+ ```
155
+
130
156
  ### Directory Structure
131
157
 
132
158
  Created automatically on first run:
@@ -152,11 +178,10 @@ ivoryos.run(__name__)
152
178
 
153
179
  ## Roadmap
154
180
 
155
- - [x] Allow plugin pages ✅
156
- - [x] pause, resume, abort current and pending workflows ✅
157
181
  - [ ] dropdown input
158
182
  - [ ] snapshot version control
159
183
  - [ ] optimizer-agnostic
184
+ - [ ] prefect compatibility
160
185
  - [ ] check batch-config file compatibility
161
186
 
162
187
  ---
@@ -198,4 +223,4 @@ For an additional perspective related to the development of the tool, please see
198
223
  ```
199
224
  ---
200
225
  ## Acknowledgements
201
- Authors acknowledge Telescope Innovations Corp., Hein Lab members for their valuable suggestions and contributions.
226
+ Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
@@ -4,7 +4,8 @@
4
4
  [![YouTube](https://img.shields.io/badge/YouTube-tutorial-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
5
5
  [![YouTube](https://img.shields.io/badge/YouTube-demo-red?logo=youtube)](https://youtu.be/flr5ydiE96s)
6
6
  [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
7
- [![Discord](https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2)](https://discord.gg/AX5P9EdGVX)
7
+
8
+ [//]: # ([![Discord]&#40;https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2&#41;]&#40;https://discord.gg/AX5P9EdGVX&#41;)
8
9
 
9
10
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
10
11
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -77,11 +78,14 @@ pip install -e .
77
78
  ## Quick start
78
79
  In your SDL script,
79
80
  ```python
81
+ my_robot = Robot()
82
+
80
83
  import ivoryos
81
84
 
82
85
  ivoryos.run(__name__)
83
86
  ```
84
- Login: Create an account (local DB, bcrypt password)
87
+ You can now access the web UI at http://127.0.0.1:8000,
88
+ create an account, login, and start designing workflows!
85
89
 
86
90
  ----
87
91
  ## Features
@@ -103,6 +107,28 @@ Add single or multiple loggers:
103
107
  ivoryos.run(__name__, logger="logger name")
104
108
  ivoryos.run(__name__, logger=["logger 1", "logger 2"])
105
109
  ```
110
+ ### Human-in-the-loop
111
+ Add single or multiple notification handlers for `pause` feature in flow control:
112
+ ```python
113
+
114
+ def slack_bot(msg: str = "Hi"):
115
+ """
116
+ a function that can be used as a notification handler function("msg")
117
+ :param msg: message to send
118
+ """
119
+ from slack_sdk import WebClient
120
+
121
+ slack_token = "your slack token"
122
+ client = WebClient(token=slack_token)
123
+
124
+ my_user_id = "your user id" # replace with your actual Slack user ID
125
+
126
+ client.chat_postMessage(channel=my_user_id, text=msg)
127
+
128
+ import ivoryos
129
+ ivoryos.run(__name__, notification_handler=slack_bot)
130
+ ```
131
+
106
132
  ### Directory Structure
107
133
 
108
134
  Created automatically on first run:
@@ -128,11 +154,10 @@ ivoryos.run(__name__)
128
154
 
129
155
  ## Roadmap
130
156
 
131
- - [x] Allow plugin pages ✅
132
- - [x] pause, resume, abort current and pending workflows ✅
133
157
  - [ ] dropdown input
134
158
  - [ ] snapshot version control
135
159
  - [ ] optimizer-agnostic
160
+ - [ ] prefect compatibility
136
161
  - [ ] check batch-config file compatibility
137
162
 
138
163
  ---
@@ -174,4 +199,4 @@ For an additional perspective related to the development of the tool, please see
174
199
  ```
175
200
  ---
176
201
  ## Acknowledgements
177
- Authors acknowledge Telescope Innovations Corp., Hein Lab members for their valuable suggestions and contributions.
202
+ Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
@@ -41,7 +41,8 @@ def reset_old_schema(engine, db_dir):
41
41
  old_workflow_run = 'workflow_runs' in tables
42
42
  old_workflow_step = 'workflow_steps' in tables
43
43
 
44
- if not has_workflow_phase:
44
+ # v1.3.4 only delete and backup when there is runs but no phases
45
+ if not has_workflow_phase and old_workflow_run:
45
46
  print("⚠️ Old workflow database detected! All previous workflows have been reset to support the new schema.")
46
47
  # Backup old DB
47
48
  db_path = os.path.join(db_dir, "ivoryos.db")
@@ -49,6 +49,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
49
49
  enable_design: bool = True,
50
50
  blueprint_plugins: Union[list, Blueprint] = [],
51
51
  exclude_names: list = [],
52
+ notification_handler=None,
52
53
  ):
53
54
  """
54
55
  Start ivoryOS app server.
@@ -65,6 +66,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
65
66
  :param enable_design: enable design canvas, database and workflow execution
66
67
  :param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
67
68
  :param exclude_names: list[str] module names to exclude from parsing
69
+ :param notification_handler: notification handler function
68
70
  """
69
71
  app = create_app(config_class=config or get_config()) # Create app instance using factory function
70
72
 
@@ -113,11 +115,33 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
113
115
  output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
114
116
  else:
115
117
  app.config["ENABLE_LLM"] = False
116
- if logger and type(logger) is str:
117
- utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
118
- elif type(logger) is list:
119
- for log in logger:
120
- utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
118
+
119
+
120
+ # --- Logger registration ---
121
+ if logger:
122
+ if isinstance(logger, str):
123
+ logger = [logger] # convert single logger to list
124
+ elif not isinstance(logger, list):
125
+ raise TypeError("logger must be a string or a list of strings.")
126
+
127
+ for log_name in logger:
128
+ utils.start_logger(socketio, log_filename=logger_path, logger_name=log_name)
129
+
130
+ # --- Notification handler registration ---
131
+ if notification_handler:
132
+
133
+ # make it a list if a single function is passed
134
+ if callable(notification_handler):
135
+ notification_handler = [notification_handler]
136
+
137
+ if not isinstance(notification_handler, list):
138
+ raise ValueError("notification_handlers must be a callable or a list of callables.")
139
+
140
+ # validate all items are callable
141
+ for handler in notification_handler:
142
+ if not callable(handler):
143
+ raise TypeError(f"Handler {handler} is not callable.")
144
+ global_config.register_notification(handler)
121
145
 
122
146
  # TODO in case Python 3.12 or higher doesn't log URL
123
147
  # if sys.version_info >= (3, 12):
@@ -129,23 +153,6 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
129
153
  # return app
130
154
 
131
155
 
132
- # def load_installed_plugins(app, socketio):
133
- # """
134
- # Dynamically load installed plugins and attach Flask-SocketIO.
135
- # """
136
- # plugin_names = []
137
- # for entry_point in entry_points().get("ivoryos.plugins", []):
138
- # plugin = entry_point.load()
139
- #
140
- # # If the plugin has an `init_socketio()` function, pass socketio
141
- # if hasattr(plugin, 'init_socketio'):
142
- # plugin.init_socketio(socketio)
143
- #
144
- # plugin_names.append(entry_point.name)
145
- # app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
146
- #
147
- # return plugin_names
148
-
149
156
 
150
157
  def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
151
158
  """
@@ -17,6 +17,8 @@ class GlobalConfig:
17
17
  cls._instance._runner_lock = threading.Lock()
18
18
  cls._instance._runner_status = None
19
19
  cls._instance._optimizers = {}
20
+ cls._instance._notification_handlers = []
21
+
20
22
  return cls._instance
21
23
 
22
24
  @property
@@ -28,6 +30,15 @@ class GlobalConfig:
28
30
  if self._deck is None:
29
31
  self._deck = value
30
32
 
33
+ def register_notification(self, handler):
34
+ if not callable(handler):
35
+ raise ValueError("Handler must be callable")
36
+ self._notification_handlers.append(handler)
37
+
38
+ @property
39
+ def notification_handlers(self):
40
+ return self._notification_handlers
41
+
31
42
  @property
32
43
  def building_blocks(self):
33
44
  return self._building_blocks
@@ -2,6 +2,20 @@ import ast
2
2
  import json
3
3
  import uuid
4
4
 
5
+
6
+ from ivoryos.utils.global_config import GlobalConfig
7
+
8
+ global_config = GlobalConfig()
9
+
10
+ if global_config.building_blocks:
11
+ building_blocks = {
12
+ inner_key: f"{block_key}.{inner_key}"
13
+ for block_key, block_value in global_config.building_blocks.items()
14
+ for inner_key in block_value.keys()
15
+ }
16
+ else:
17
+ building_blocks = {}
18
+
5
19
  def generate_uuid():
6
20
  return int(str(uuid.uuid4().int)[:15])
7
21
 
@@ -37,7 +51,7 @@ def convert_to_cards(source_code: str):
37
51
  def is_supported_assignment(node):
38
52
  return (
39
53
  isinstance(node.targets[0], ast.Name) and
40
- isinstance(node.value, (ast.Constant, ast.Num, ast.Str, ast.NameConstant))
54
+ isinstance(node.value, ast.Constant)
41
55
  )
42
56
 
43
57
  class CardVisitor(ast.NodeVisitor):
@@ -143,11 +157,22 @@ def convert_to_cards(source_code: str):
143
157
  f = f.value
144
158
  if isinstance(f, ast.Name):
145
159
  func_parts.insert(0, f.id)
146
- if not func_parts:
160
+
161
+ full_func_name = ".".join(func_parts)
162
+
163
+ # Check if this is a deck call or a building block
164
+ if full_func_name.startswith("deck.") or full_func_name.startswith("blocks."):
165
+ instrument = ".".join(func_parts[:-1])
166
+ action = func_parts[-1]
167
+ # not starting with deck or block, check if it's a decorated function
168
+ # ["general", "action"] or ["action"]
169
+ elif func_parts[-1] in building_blocks.keys():
170
+ instrument = building_blocks.get(func_parts[-1])
171
+ action = func_parts[-1]
172
+ else:
173
+ # ignore other calls
147
174
  return
148
175
 
149
- instrument = ".".join(func_parts[:-1])
150
- action = func_parts[-1]
151
176
 
152
177
 
153
178
  # --- special case for time.sleep ---
@@ -181,7 +206,7 @@ def convert_to_cards(source_code: str):
181
206
  for kw in node.keywords:
182
207
  if kw.arg is None and isinstance(kw.value, ast.Dict):
183
208
  for k_node, v_node in zip(kw.value.keys, kw.value.values):
184
- key = k_node.s if isinstance(k_node, ast.Constant) else ast.unparse(k_node)
209
+ key = k_node.value if isinstance(k_node, ast.Constant) else ast.unparse(k_node)
185
210
  if isinstance(v_node, ast.Constant):
186
211
  value = v_node.value
187
212
  elif isinstance(v_node, ast.Name):
@@ -242,10 +267,14 @@ if __name__ == "__main__":
242
267
  # Step 4: Analyze the sample
243
268
  analysis_results = deck.sdl.analyze(param_1=1, param_2=2)
244
269
 
270
+ # test block
271
+ result = blocks.general.test(**{'a': 1, 'b': 2})
272
+
245
273
  # Brief pause for system stability
246
274
  time.sleep(1.0)
247
275
 
248
276
  # Return only analysis results
249
277
  return {'analysis_results': analysis_results}
250
278
  '''
251
- print(json.dumps(convert_to_cards(test)))
279
+ from pprint import pprint
280
+ pprint(json.dumps(convert_to_cards(test)))
@@ -19,6 +19,14 @@ class HumanInterventionRequired(Exception):
19
19
  pass
20
20
 
21
21
  def pause(reason="Human intervention required"):
22
+ handlers = global_config.notification_handlers
23
+ if handlers:
24
+ for handler in handlers:
25
+ try:
26
+ handler(reason)
27
+ except Exception as e:
28
+ print(f"[notify] handler {handler} failed: {e}")
29
+ # raise error to pause workflow in gui
22
30
  raise HumanInterventionRequired(reason)
23
31
 
24
32
  class ScriptRunner:
@@ -161,7 +169,7 @@ class ScriptRunner:
161
169
  start_time=datetime.now(),
162
170
  )
163
171
  db.session.add(step)
164
- db.session.commit()
172
+ db.session.flush()
165
173
 
166
174
  logger.info(f"Executing: {line}")
167
175
  socketio.emit('execution', {'section': f"{section_name}-{index}"})
@@ -219,7 +227,7 @@ class ScriptRunner:
219
227
  repeat_mode=repeat_mode
220
228
  )
221
229
  db.session.add(run)
222
- db.session.commit()
230
+ db.session.flush()
223
231
  run_id = run.id # Save the ID
224
232
  try:
225
233
 
@@ -277,7 +285,7 @@ class ScriptRunner:
277
285
  start_time=datetime.now()
278
286
  )
279
287
  db.session.add(phase)
280
- db.session.commit()
288
+ db.session.flush()
281
289
  phase_id = phase.id
282
290
 
283
291
  step_outputs = self.exec_steps(script, section_name, logger, socketio, phase_id=phase_id)
@@ -317,7 +325,7 @@ class ScriptRunner:
317
325
  start_time=datetime.now()
318
326
  )
319
327
  db.session.add(phase)
320
- db.session.commit()
328
+ db.session.flush()
321
329
 
322
330
  phase_id = phase.id
323
331
  output = self.exec_steps(script, "script", logger, socketio, phase_id, **kwargs)
@@ -371,7 +379,7 @@ class ScriptRunner:
371
379
  start_time=datetime.now()
372
380
  )
373
381
  db.session.add(phase)
374
- db.session.commit()
382
+ db.session.flush()
375
383
  phase_id = phase.id
376
384
 
377
385
  logger.info(f'Executing {run_name} experiment: {i_progress + 1}/{int(repeat_count)}')
@@ -29,6 +29,7 @@ class TaskRunner:
29
29
  if not self.lock.acquire(blocking=False):
30
30
  current_status = global_config.runner_status
31
31
  current_status["status"] = "busy"
32
+ current_status["output"] = "busy"
32
33
  return current_status
33
34
 
34
35
 
@@ -71,7 +72,7 @@ class TaskRunner:
71
72
  with current_app.app_context():
72
73
  step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=None, start_time=datetime.now())
73
74
  db.session.add(step)
74
- db.session.commit()
75
+ db.session.flush()
75
76
  global_config.runner_status = {"id":step.id, "type": "task"}
76
77
  try:
77
78
  output = function_executable(**kwargs)
@@ -141,6 +141,7 @@ def _get_type_from_parameters(arg, parameters):
141
141
  def _convert_by_str(args, arg_types):
142
142
  """
143
143
  Converts a value to type through eval(f'{type}("{args}")')
144
+ v1.3.4 TODO try str lastly, otherwise it's always converted to str
144
145
  """
145
146
  if type(arg_types) is not list:
146
147
  arg_types = [arg_types]
@@ -0,0 +1 @@
1
+ __version__ = "1.3.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -28,7 +28,8 @@ Dynamic: license-file
28
28
  [![YouTube](https://img.shields.io/badge/YouTube-tutorial-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
29
29
  [![YouTube](https://img.shields.io/badge/YouTube-demo-red?logo=youtube)](https://youtu.be/flr5ydiE96s)
30
30
  [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
31
- [![Discord](https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2)](https://discord.gg/AX5P9EdGVX)
31
+
32
+ [//]: # ([![Discord]&#40;https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2&#41;]&#40;https://discord.gg/AX5P9EdGVX&#41;)
32
33
 
33
34
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
34
35
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -101,11 +102,14 @@ pip install -e .
101
102
  ## Quick start
102
103
  In your SDL script,
103
104
  ```python
105
+ my_robot = Robot()
106
+
104
107
  import ivoryos
105
108
 
106
109
  ivoryos.run(__name__)
107
110
  ```
108
- Login: Create an account (local DB, bcrypt password)
111
+ You can now access the web UI at http://127.0.0.1:8000,
112
+ create an account, login, and start designing workflows!
109
113
 
110
114
  ----
111
115
  ## Features
@@ -127,6 +131,28 @@ Add single or multiple loggers:
127
131
  ivoryos.run(__name__, logger="logger name")
128
132
  ivoryos.run(__name__, logger=["logger 1", "logger 2"])
129
133
  ```
134
+ ### Human-in-the-loop
135
+ Add single or multiple notification handlers for `pause` feature in flow control:
136
+ ```python
137
+
138
+ def slack_bot(msg: str = "Hi"):
139
+ """
140
+ a function that can be used as a notification handler function("msg")
141
+ :param msg: message to send
142
+ """
143
+ from slack_sdk import WebClient
144
+
145
+ slack_token = "your slack token"
146
+ client = WebClient(token=slack_token)
147
+
148
+ my_user_id = "your user id" # replace with your actual Slack user ID
149
+
150
+ client.chat_postMessage(channel=my_user_id, text=msg)
151
+
152
+ import ivoryos
153
+ ivoryos.run(__name__, notification_handler=slack_bot)
154
+ ```
155
+
130
156
  ### Directory Structure
131
157
 
132
158
  Created automatically on first run:
@@ -152,11 +178,10 @@ ivoryos.run(__name__)
152
178
 
153
179
  ## Roadmap
154
180
 
155
- - [x] Allow plugin pages ✅
156
- - [x] pause, resume, abort current and pending workflows ✅
157
181
  - [ ] dropdown input
158
182
  - [ ] snapshot version control
159
183
  - [ ] optimizer-agnostic
184
+ - [ ] prefect compatibility
160
185
  - [ ] check batch-config file compatibility
161
186
 
162
187
  ---
@@ -198,4 +223,4 @@ For an additional perspective related to the development of the tool, please see
198
223
  ```
199
224
  ---
200
225
  ## Acknowledgements
201
- Authors acknowledge Telescope Innovations Corp., Hein Lab members for their valuable suggestions and contributions.
226
+ Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
@@ -1 +0,0 @@
1
- __version__ = "1.3.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes