ivoryos 0.1.5__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 (46) hide show
  1. ivoryos/__init__.py +94 -0
  2. ivoryos/config.py +46 -0
  3. ivoryos/routes/__init__.py +0 -0
  4. ivoryos/routes/auth/__init__.py +0 -0
  5. ivoryos/routes/auth/auth.py +65 -0
  6. ivoryos/routes/auth/templates/auth/login.html +25 -0
  7. ivoryos/routes/auth/templates/auth/signup.html +32 -0
  8. ivoryos/routes/control/__init__.py +0 -0
  9. ivoryos/routes/control/control.py +233 -0
  10. ivoryos/routes/control/templates/control/controllers.html +71 -0
  11. ivoryos/routes/control/templates/control/controllers_home.html +50 -0
  12. ivoryos/routes/control/templates/control/controllers_new.html +89 -0
  13. ivoryos/routes/database/__init__.py +0 -0
  14. ivoryos/routes/database/database.py +122 -0
  15. ivoryos/routes/database/templates/database/experiment_database.html +72 -0
  16. ivoryos/routes/design/__init__.py +0 -0
  17. ivoryos/routes/design/design.py +396 -0
  18. ivoryos/routes/design/templates/design/experiment_builder.html +413 -0
  19. ivoryos/routes/design/templates/design/experiment_run.html +325 -0
  20. ivoryos/routes/main/__init__.py +0 -0
  21. ivoryos/routes/main/main.py +25 -0
  22. ivoryos/routes/main/templates/main/help.html +144 -0
  23. ivoryos/routes/main/templates/main/home.html +68 -0
  24. ivoryos/static/favicon.ico +0 -0
  25. ivoryos/static/gui_annotation/Slide1.png +0 -0
  26. ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  27. ivoryos/static/js/overlay.js +12 -0
  28. ivoryos/static/js/socket_handler.js +25 -0
  29. ivoryos/static/js/sortable_card.js +24 -0
  30. ivoryos/static/js/sortable_design.js +36 -0
  31. ivoryos/static/logo.png +0 -0
  32. ivoryos/static/style.css +202 -0
  33. ivoryos/templates/base.html +141 -0
  34. ivoryos/utils/__init__.py +0 -0
  35. ivoryos/utils/db_models.py +501 -0
  36. ivoryos/utils/form.py +316 -0
  37. ivoryos/utils/global_config.py +68 -0
  38. ivoryos/utils/llm_agent.py +183 -0
  39. ivoryos/utils/script_runner.py +158 -0
  40. ivoryos/utils/task_manager.py +80 -0
  41. ivoryos/utils/utils.py +337 -0
  42. ivoryos-0.1.5.dist-info/LICENSE +21 -0
  43. ivoryos-0.1.5.dist-info/METADATA +96 -0
  44. ivoryos-0.1.5.dist-info/RECORD +46 -0
  45. ivoryos-0.1.5.dist-info/WHEEL +5 -0
  46. ivoryos-0.1.5.dist-info/top_level.txt +1 -0
ivoryos/utils/form.py ADDED
@@ -0,0 +1,316 @@
1
+ from wtforms.fields.core import Field
2
+ from wtforms.utils import UnsetValue
3
+ from wtforms.validators import InputRequired
4
+ from wtforms.widgets.core import TextInput
5
+
6
+ from flask_wtf import FlaskForm
7
+ from wtforms import StringField, FloatField, HiddenField, BooleanField, IntegerField
8
+ import inspect
9
+
10
+
11
+ def find_variable(data, script):
12
+ # TODO: needs to check for valid order of variables, important when editting
13
+ added_variables: list[dict[str, str]] = [action for action in script.currently_editing_script if
14
+ action["instrument"] == "variable"
15
+ # or action["return"] # TODO find returns
16
+ ]
17
+ for added_variable in added_variables:
18
+ if added_variable["action"] == data:
19
+ return data, added_variable["args"]
20
+ # if added_variable["return"] == data:
21
+ # return data, None
22
+ return None, None
23
+
24
+
25
+ class VariableOrStringField(Field):
26
+ widget = TextInput()
27
+
28
+ def __init__(self, label='', validators=None, script=None, **kwargs):
29
+ super(VariableOrStringField, self).__init__(label, validators, **kwargs)
30
+ self.script = script
31
+
32
+ def process_formdata(self, valuelist):
33
+ if valuelist:
34
+ if not self.script.editing_type == "script" and valuelist[0].startswith("#"):
35
+ raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
36
+ self.data = valuelist[0]
37
+
38
+ def _value(self):
39
+ if self.script:
40
+ variable, value = find_variable(self.data, self.script)
41
+ if variable:
42
+ return variable
43
+
44
+ return str(self.data) if self.data is not None else ""
45
+
46
+
47
+ class VariableOrFloatField(Field):
48
+ widget = TextInput()
49
+
50
+ def __init__(self, label='', validators=None, script=None, **kwargs):
51
+ super(VariableOrFloatField, self).__init__(label, validators, **kwargs)
52
+ self.script = script
53
+
54
+ def _value(self):
55
+ if self.script:
56
+ variable, value = find_variable(self.data, self.script)
57
+ if variable:
58
+ return variable
59
+
60
+ if self.raw_data:
61
+ return self.raw_data[0]
62
+ if self.data is not None:
63
+ return str(self.data)
64
+ return ""
65
+
66
+ def process_formdata(self, valuelist):
67
+ if not valuelist:
68
+ return
69
+ elif valuelist[0].startswith("#"):
70
+ if not self.script.editing_type == "script":
71
+ raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
72
+ self.data = valuelist[0]
73
+ return
74
+ try:
75
+ if self.script:
76
+ try:
77
+ variable, value = find_variable(valuelist[0], self.script)
78
+ if variable:
79
+ float(value)
80
+ self.data = str(variable)
81
+ return
82
+ except ValueError:
83
+ pass
84
+
85
+ self.data = float(valuelist[0])
86
+ except ValueError as exc:
87
+ self.data = None
88
+ raise ValueError(self.gettext("Not a valid float value.")) from exc
89
+
90
+
91
+ unset_value = UnsetValue()
92
+
93
+
94
+ class VariableOrIntField(Field):
95
+ widget = TextInput()
96
+
97
+ def __init__(self, label='', validators=None, script=None, **kwargs):
98
+ super(VariableOrIntField, self).__init__(label, validators, **kwargs)
99
+ self.script = script
100
+
101
+ def _value(self):
102
+ if self.script:
103
+ variable, value = find_variable(self.data, self.script)
104
+ if variable:
105
+ return variable
106
+
107
+ if self.raw_data:
108
+ return self.raw_data[0]
109
+ if self.data is not None:
110
+ return str(self.data)
111
+ return ""
112
+
113
+ # def process_data(self, value):
114
+ #
115
+ # if self.script:
116
+ # variable, var_value = find_variable(value, self.script)
117
+ # if variable:
118
+ # try:
119
+ # int(var_value)
120
+ # self.data = str(variable)
121
+ # return
122
+ # except ValueError:
123
+ # pass
124
+ # if value is None or value is unset_value:
125
+ # self.data = None
126
+ # return
127
+ # try:
128
+ # self.data = int(value)
129
+ # except (ValueError, TypeError) as exc:
130
+ # self.data = None
131
+ # raise ValueError(self.gettext("Not a valid integer value.")) from exc
132
+
133
+ def process_formdata(self, valuelist):
134
+ if not valuelist:
135
+ return
136
+ if self.script:
137
+ variable, var_value = find_variable(valuelist[0], self.script)
138
+ if variable:
139
+ try:
140
+ int(var_value)
141
+ self.data = str(variable)
142
+ return
143
+ except ValueError:
144
+ pass
145
+ if valuelist[0].startswith("#"):
146
+ if not self.script.editing_type == "script":
147
+ raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
148
+ self.data = valuelist[0]
149
+ return
150
+ try:
151
+ self.data = int(valuelist[0])
152
+ except ValueError as exc:
153
+ self.data = None
154
+ raise ValueError(self.gettext("Not a valid integer value.")) from exc
155
+
156
+
157
+ class VariableOrBoolField(BooleanField):
158
+ widget = TextInput()
159
+
160
+ def __init__(self, label='', validators=None, script=None, **kwargs):
161
+ super(VariableOrBoolField, self).__init__(label, validators, **kwargs)
162
+ self.script = script
163
+
164
+ def process_data(self, value):
165
+
166
+ if self.script:
167
+ variable, var_value = find_variable(value, self.script)
168
+ if variable:
169
+ try:
170
+ bool(var_value)
171
+ return variable
172
+ except ValueError:
173
+ return
174
+
175
+ self.data = bool(value)
176
+
177
+ def process_formdata(self, valuelist):
178
+ if not valuelist or type(valuelist) is list and valuelist[0] == '':
179
+ self.data = False
180
+ elif valuelist and valuelist[0].startswith("#"):
181
+ if not self.script.editing_type == "script":
182
+ raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
183
+ self.data = valuelist[0]
184
+ else:
185
+ self.data = True
186
+
187
+ def _value(self):
188
+
189
+ if self.script:
190
+ variable, value = find_variable(self.raw_data, self.script)
191
+ if variable:
192
+ return variable
193
+
194
+ if self.raw_data:
195
+ return str(self.raw_data[0])
196
+ return "y"
197
+
198
+
199
+ def format_name(name):
200
+ """Converts 'example_name' to 'Example Name'."""
201
+ name = name.split(".")[-1]
202
+ text = ' '.join(word for word in name.split('_'))
203
+ return text.capitalize()
204
+
205
+
206
+ def create_form_for_method(method, method_name, autofill, script=None, design=True):
207
+ class DynamicForm(FlaskForm):
208
+ pass
209
+
210
+ annotation_mapping = {
211
+ int: (VariableOrIntField if design else IntegerField, 'Enter integer value'),
212
+ float: (VariableOrFloatField if design else FloatField, 'Enter numeric value'),
213
+ str: (VariableOrStringField if design else StringField, 'Enter text'),
214
+ bool: (VariableOrBoolField if design else BooleanField, 'Empty for false')
215
+ }
216
+ sig = method if type(method) is inspect.Signature else inspect.signature(method)
217
+
218
+ for param in sig.parameters.values():
219
+ if param.name == 'self':
220
+ continue
221
+ formatted_param_name = format_name(param.name)
222
+ field_kwargs = {
223
+ "label": formatted_param_name,
224
+ "default": f'#{param.name}' if autofill else (param.default if param.default is not param.empty else ""),
225
+ **({"script": script} if (autofill or design) else {})
226
+ }
227
+ field_class, placeholder_text = annotation_mapping.get(
228
+ param.annotation,
229
+ (VariableOrStringField if design else StringField, f'Enter {param.annotation} value')
230
+ )
231
+ render_kwargs = {"placeholder": placeholder_text}
232
+
233
+ # Create the field with additional rendering kwargs for placeholder text
234
+ field = field_class(**field_kwargs, render_kw=render_kwargs)
235
+ setattr(DynamicForm, param.name, field)
236
+
237
+ # setattr(DynamicForm, f'add', fname)
238
+ return DynamicForm
239
+
240
+
241
+ # Create forms for each method in DummySDLDeck
242
+ def create_add_form(attr, attr_name, autofill, script=None, design=True):
243
+ dynamic_form = create_form_for_method(attr, attr_name, autofill, script, design)
244
+ if design:
245
+ return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
246
+ setattr(dynamic_form, 'return', return_value)
247
+ hidden_method_name = HiddenField(name=f'hidden_name', render_kw={"value": f'{attr_name}'})
248
+ setattr(dynamic_form, 'hidden_name', hidden_method_name)
249
+ return dynamic_form
250
+
251
+
252
+ def create_form_from_module(sdl_module, autofill: bool, script=None, design=True):
253
+ # sdl_deck = DummySDLDeck(DummyPump("COM1"), DummyBalance("COM2"))
254
+ method_forms = {}
255
+ for attr_name in dir(sdl_module):
256
+ attr = getattr(sdl_module, attr_name)
257
+ if inspect.ismethod(attr) and not attr_name.startswith('_'):
258
+ form_class = create_add_form(attr, attr_name, autofill, script, design)
259
+ method_forms[attr_name] = form_class()
260
+ return method_forms
261
+
262
+
263
+ def create_form_from_pseudo(pseudo: dict, autofill: bool, script=None, design=True):
264
+ '''{'dose_liquid': < Signature(amount_in_ml: float, rate_ml_per_minute: float) >}'''
265
+ method_forms = {}
266
+ for attr_name, signature in pseudo.items():
267
+ form_class = create_add_form(signature, attr_name, autofill, script, design)
268
+ method_forms[attr_name] = form_class()
269
+ return method_forms
270
+
271
+
272
+ def create_builtin_form(logic_type):
273
+ class BuiltinFunctionForm(FlaskForm):
274
+ pass
275
+
276
+ placeholder_text = f'Enter numbers' if logic_type == 'wait' else f'Enter statement'
277
+ description_text = f'Your variable can be numbers, boolean (True or False) or text ("text")' if logic_type == 'variable' else ''
278
+ field_class = FloatField if logic_type == 'wait' else StringField # Default to StringField as a fallback
279
+ field_kwargs = {
280
+ "label": f'statement',
281
+ "validators": [InputRequired()] if logic_type in ['wait', "variable"] else [],
282
+ "description": description_text,
283
+ }
284
+ render_kwargs = {"placeholder": placeholder_text}
285
+ field = field_class(**field_kwargs, render_kw=render_kwargs)
286
+ setattr(BuiltinFunctionForm, "statement", field)
287
+ if logic_type == 'variable':
288
+ variable_field = StringField(label=f'variable', validators=[InputRequired()],
289
+ description="Your variable name cannot include space",
290
+ render_kw=render_kwargs)
291
+ setattr(BuiltinFunctionForm, "variable", variable_field)
292
+ hidden_field = HiddenField(name=f'builtin_name', render_kw={"value": f'{logic_type}'})
293
+ setattr(BuiltinFunctionForm, "builtin_name", hidden_field)
294
+ return BuiltinFunctionForm()
295
+
296
+
297
+ def create_action_button(s: dict):
298
+ style = ""
299
+ if s['instrument'] in ['if', 'while']:
300
+ text = f"{s['action']} {s['args']}"
301
+ style = "background-color: tomato"
302
+ elif s['instrument'] == 'variable':
303
+ text = f"{s['action']} = {s['args']}"
304
+ else:
305
+ # regular action button
306
+ prefix = f"{s['return']} = " if s['return'] else ""
307
+ action_text = f"{s['instrument'].split('.')[-1] if s['instrument'].startswith('deck') else s['instrument']}.{s['action']}"
308
+ arg_string = ""
309
+ if s['args']:
310
+ if type(s['args']) is dict:
311
+ arg_string = "(" + ", ".join([f"{k} = {v}" for k, v in s['args'].items()]) + ")"
312
+ else:
313
+ arg_string = f"= {s['args']}"
314
+
315
+ text = f"{prefix}{action_text} {arg_string}"
316
+ return dict(label=text, style=style, uuid=s["uuid"], id=s["id"])
@@ -0,0 +1,68 @@
1
+ # from ivoryos.utils.script_runner import ScriptRunner
2
+
3
+
4
+ class GlobalConfig:
5
+ _instance = None
6
+
7
+ def __new__(cls, *args, **kwargs):
8
+ if cls._instance is None:
9
+ cls._instance = super(GlobalConfig, cls).__new__(cls, *args, **kwargs)
10
+ cls._instance._deck = None
11
+ cls._instance._agent = None
12
+ cls._instance._defined_variables = {}
13
+ cls._instance._api_variables = set()
14
+ cls._instance._deck_variables = {}
15
+ cls._instance._runner = None
16
+ return cls._instance
17
+
18
+ @property
19
+ def deck(self):
20
+ return self._deck
21
+
22
+ @deck.setter
23
+ def deck(self, value):
24
+ if self._deck is None:
25
+ self._deck = value
26
+
27
+
28
+ @property
29
+ def deck_variables(self):
30
+ return self._deck_variables
31
+
32
+ @deck_variables.setter
33
+ def deck_variables(self, value):
34
+ self._deck_variables = value
35
+
36
+
37
+ @property
38
+ def agent(self):
39
+ return self._agent
40
+
41
+ @agent.setter
42
+ def agent(self, value):
43
+ if self._agent is None:
44
+ self._agent = value
45
+
46
+ @property
47
+ def defined_variables(self):
48
+ return self._defined_variables
49
+
50
+ @defined_variables.setter
51
+ def defined_variables(self, value):
52
+ self._defined_variables = value
53
+
54
+ @property
55
+ def api_variables(self):
56
+ return self._api_variables
57
+
58
+ @api_variables.setter
59
+ def api_variables(self, value):
60
+ self._api_variables = value
61
+
62
+ @property
63
+ def runner(self):
64
+ return self._runner
65
+
66
+ @runner.setter
67
+ def runner(self, value):
68
+ self._runner = value
@@ -0,0 +1,183 @@
1
+ import inspect
2
+ import json
3
+ import os
4
+ import re
5
+
6
+ from openai import OpenAI, BaseModel
7
+
8
+
9
+ # from dotenv import load_dotenv
10
+ # load_dotenv()
11
+
12
+ # host = "137.82.65.246"
13
+ # model = "llama3"
14
+
15
+ # structured output,
16
+ # class Action(BaseModel):
17
+ # action: str
18
+ # args: dict
19
+ # arg_types: dict
20
+ #
21
+ #
22
+ # class ActionPlan(BaseModel):
23
+ # actions: list[Action]
24
+ # # final_answer: str
25
+
26
+
27
+ class LlmAgent:
28
+ def __init__(self, model="llama3", output_path=os.curdir, host=None):
29
+ self.host = host
30
+ self.base_url = f"http://{self.host}:11434/v1/" if host is not None else ""
31
+ self.model = model
32
+ self.output_path = os.path.join(output_path, "llm_output") if output_path is not None else None
33
+ self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) if host is None else OpenAI(api_key="ollama",
34
+ base_url=self.base_url)
35
+ if self.output_path is not None:
36
+ os.makedirs(self.output_path, exist_ok=True)
37
+
38
+ @staticmethod
39
+ def extract_annotations_docstrings(module_sigs):
40
+ class_str = ""
41
+
42
+ for name, value in module_sigs.items():
43
+ signature = value.get("signature")
44
+ docstring = value.get("docstring")
45
+ class_str += f'\tdef {name}{signature}:\n'
46
+ class_str += f'\t\t"""\n\t\t{docstring}\n\t\t"""' + '\n' if docstring else ''
47
+ class_str = class_str.replace('self, ', '')
48
+ class_str = class_str.replace('self', '')
49
+ name_list = list(module_sigs.keys())
50
+ # print(class_str)
51
+ # with open(os.path.join(self.output_path, "docstring_manual.txt"), "w") as f:
52
+ # f.write(class_str)
53
+ return class_str, name_list
54
+
55
+ @staticmethod
56
+ def parse_code_from_msg(msg):
57
+ msg = msg.strip()
58
+ # print(msg)
59
+ # code_blocks = re.findall(r'```(?:json\s)?(.*?)```', msg, re.DOTALL)
60
+ code_blocks = re.findall(r'\[\s*\{.*?\}\s*\]', msg, re.DOTALL)
61
+
62
+ json_blocks = []
63
+ for block in code_blocks:
64
+ if not block.startswith('['):
65
+ start_index = block.find('[')
66
+ block = block[start_index:]
67
+ block = re.sub(r'//.*', '', block)
68
+ block = block.replace('True', 'true').replace('False', 'false')
69
+ try:
70
+ # Try to parse the block as JSON
71
+ json_data = json.loads(block.strip())
72
+ if isinstance(json_data, list):
73
+ json_blocks = json_data
74
+ except json.JSONDecodeError:
75
+ continue
76
+ return json_blocks
77
+
78
+ def _generate(self, robot_sigs, prompt):
79
+ # deck_info, name_list = self.extract_annotations_docstrings(type(robot))
80
+ deck_info, name_list = self.extract_annotations_docstrings(robot_sigs)
81
+ full_prompt = '''I have some python functions, for example when calling them I want to write them using JSON,
82
+ it is necessary to include all args
83
+ for example
84
+ def dose_solid(amount_in_mg:float, bring_in:bool=True): def analyze():
85
+ dose_solid(3)
86
+ analyze()
87
+ I would want to write to
88
+ [
89
+ {
90
+ "action": "dose_solid",
91
+ "arg_types": {
92
+ "amount_in_mg": "float",
93
+ "bring_in": "bool"
94
+ },
95
+ "args": {
96
+ "amount_in_mg": 3,
97
+ "bring_in": true
98
+ }
99
+ },
100
+ {
101
+ "action": "analyze",
102
+ "arg_types": {},
103
+ "args": {}
104
+ }
105
+ ]
106
+ ''' + f'''
107
+ Now these are my callable functions,
108
+ {deck_info}
109
+ and I want you to find the most appropriate function if I want to do these tasks
110
+ """{prompt}"""
111
+ ,and write a list of dictionary in json accordingly. Please only use these action names {name_list},
112
+ can you also help find the default value you can't find the info from my request.
113
+ '''
114
+ if self.output_path is not None:
115
+ with open(os.path.join(self.output_path, "prompt.txt"), "w") as f:
116
+ f.write(full_prompt)
117
+ messages = [{"role": "user",
118
+ "content": full_prompt}, ]
119
+ # if self.host == "openai":
120
+ output = self.client.chat.completions.create(
121
+ messages=messages,
122
+ model=self.model,
123
+ # response_format={"type": "json_object"},
124
+ )
125
+ msg = output.choices[0].message.content
126
+ # msg = output.choices[0].message.parsed
127
+
128
+ code = self.parse_code_from_msg(msg)
129
+ code = [action for action in code if action.get('action', '') in name_list]
130
+ # print('\033[91m', code, '\033[0m')
131
+ return code
132
+
133
+ def generate_code(self, robot_signature, prompt, attempt_allowance: int = 3):
134
+ attempt = 0
135
+
136
+ while attempt < attempt_allowance:
137
+ _code = self._generate(robot_signature, prompt)
138
+ attempt += 1
139
+ if _code:
140
+ break
141
+
142
+ return self.fill_blanks(_code, robot_signature)
143
+ # return code
144
+
145
+ @staticmethod
146
+ def fill_blanks(actions, robot_signature):
147
+ for action in actions:
148
+ action_name = action['action']
149
+ action_signature = robot_signature.get(action_name).get('signature', {})
150
+ args = action.get("args", {})
151
+ arg_types = action.get("arg_types", {})
152
+ for param in action_signature.parameters.values():
153
+ if param.name == 'self':
154
+ continue
155
+ if param.name not in args:
156
+ args[param.name] = param.default if param.default is not param.empty else ''
157
+ arg_types[param.name] = param.annotation.__name__
158
+ action['args'] = args
159
+ action['arg_types'] = arg_types
160
+ return actions
161
+
162
+
163
+ if __name__ == "__main__":
164
+ from pprint import pprint
165
+ from example.sdl_example.abstract_sdl import deck
166
+
167
+ from utils import parse_functions
168
+
169
+ deck_sig = parse_functions(deck, doc_string=True)
170
+ # llm_agent = LlmAgent(host="openai", model="gpt-3.5-turbo")
171
+ llm_agent = LlmAgent(host="localhost", model="llama3.1")
172
+ # robot = IrohDeck()
173
+ # extract_annotations_docstrings(DummySDLDeck)
174
+ prompt = '''I want to start with dosing 10 mg of current sample, and add 1 mL of toluene
175
+ and equilibrate for 10 minute at 40 degrees, then sample 20 ul of sample to analyze with hplc, and save result'''
176
+ code = llm_agent.generate_code(deck_sig, prompt)
177
+ pprint(code)
178
+
179
+ """
180
+ I want to dose 10mg, 6mg, 4mg, 3mg, 2mg, 1mg to 6 vials
181
+ I want to add 10 mg to vial a3, and 10 ml of liquid, then shake them for 3 minutes
182
+
183
+ """