enricorampazzo commited on
Commit
ebaa573
1 Parent(s): e338642

now it is possible to add details via the UI

Browse files
app.py CHANGED
@@ -2,15 +2,17 @@ import re
2
  from enum import Enum
3
 
4
  import streamlit as st
 
5
  from streamlit import session_state as ss
 
6
  from streamlit.runtime.scriptrunner_utils.exceptions import StopException
7
 
8
  from form.form import build_form_data_from_answers, write_pdf_form, work_categories
9
  from llm_manager.llm_parser import LlmParser
10
- from local_storage.entities import PersonalDetails, LocationDetails, ContractorDetails
11
  from local_storage.ls_manager import LocalStorageManager
12
  from prompts.prompts_manager import PromptsManager
13
- from enums import Questions as Q
14
  from repository.repository import build_repo_from_environment, get_repository
15
  from repository import ModelRoles, Model
16
  from utils.parsing_utils import check_for_missing_answers
@@ -19,6 +21,7 @@ user_msg = "Please describe what you need to do. To get the best results try to
19
 
20
  find_tags_regex = re.compile(r"@(\S*)")
21
 
 
22
  class Steps(Enum):
23
  INITIAL_STATE = 1
24
  PARSING_ANSWERS = 2
@@ -44,7 +47,9 @@ class UIManager:
44
  get_repository("testing",
45
  Model("fakeModel",
46
  ModelRoles("a", "b", "c"))))
 
47
  self.lsm = None
 
48
  @staticmethod
49
  def get_current_step() -> int:
50
  try:
@@ -56,28 +61,33 @@ class UIManager:
56
  def set_current_step(step: Steps):
57
  ss["step"] = step.value
58
 
59
- @staticmethod
60
- def _build_base_ui():
61
  st.markdown("## Dubai Asset Management red tape cutter")
 
 
 
 
 
 
 
 
62
 
63
  def build_ui_for_initial_state(self, user_message):
64
  help_ = user_message + "\n".join(self.pm.questions)
65
  self._build_base_ui()
66
  with st.form("Please describe your request"):
67
- user_input = st.text_area("Your input", height=700, label_visibility="hidden", placeholder=help_,
68
- help=help_)
69
- signature = st.file_uploader("Your signature", key="file_upload")
70
- ss["signature"] = signature
71
- submit_button = st.form_submit_button()
72
- if submit_button:
73
- ss["user_input"] = user_input
74
- self.set_current_step(Steps.PARSING_ANSWERS)
75
- st.rerun()
76
 
77
  def build_ui_for_parsing_answers(self):
78
  self._build_base_ui()
79
  with st.status("parsing user input for tags"):
80
- self.lsm = LocalStorageManager() if not self.lsm else self.lsm
81
  tags = find_tags_regex.findall(ss["user_input"])
82
  details = [self.lsm.get_detail(t) for t in tags]
83
  with st.status("initialising LLM"):
@@ -165,12 +175,12 @@ class UIManager:
165
  self._build_validation_form(True, self._create_pdf_form,
166
  "Create work permit request")
167
 
168
-
169
  @staticmethod
170
  def _get_personal_details(personal_details_key) -> PersonalDetails | None:
171
  key_ = ss.get(personal_details_key)
172
  if key_:
173
- details = PersonalDetails(ss[f"fq_{Q.FULL_NAME.name}"], ss[f"fq_{Q.YOUR_EMAIL.name}"], ss[f"fq_{Q.CONTACT_NUMBER.name}"])
 
174
  return details
175
  return None
176
 
@@ -188,9 +198,7 @@ class UIManager:
188
  ss[f"fq_{Q.COMPANY_EMAIL.name}"])
189
  return None
190
 
191
-
192
-
193
- def _build_validation_form(self, show_categories:bool, onsubmit, submit_button_label):
194
  def build_form_fragment(form_, col, title, add_save, *questions):
195
  form_.text(title)
196
  for user_data in questions:
@@ -206,7 +214,7 @@ class UIManager:
206
  col1, col2 = f.columns(2)
207
  build_form_fragment(f, col1, "your details", True, Q.FULL_NAME, Q.CONTACT_NUMBER, Q.YOUR_EMAIL)
208
  build_form_fragment(f, col2, "work details", False, Q.WORK_TO_DO, Q.START_DATE, Q.END_DATE)
209
- build_form_fragment(f, col1, "location details", True, Q.COMMUNITY, Q.BUILDING, Q.UNIT_APT_NUMBER,
210
  Q.OWNER_OR_TENANT)
211
  build_form_fragment(f, col2, "contractor details", True, Q.COMPANY_NAME, Q.COMPANY_NUMBER, Q.COMPANY_EMAIL)
212
  if show_categories:
@@ -217,12 +225,69 @@ class UIManager:
217
  if submit_data:
218
  onsubmit()
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  um = UIManager()
222
 
223
- def use_streamlit():
224
 
225
- if um.get_current_step() == Steps.INITIAL_STATE.value :
 
226
  um.build_ui_for_initial_state(user_msg)
227
  elif um.get_current_step() == Steps.PARSING_ANSWERS.value:
228
  um.build_ui_for_parsing_answers()
 
2
  from enum import Enum
3
 
4
  import streamlit as st
5
+ import streamlit_tags as st_tags
6
  from streamlit import session_state as ss
7
+ from streamlit.delta_generator import DeltaGenerator
8
  from streamlit.runtime.scriptrunner_utils.exceptions import StopException
9
 
10
  from form.form import build_form_data_from_answers, write_pdf_form, work_categories
11
  from llm_manager.llm_parser import LlmParser
12
+ from local_storage.entities import PersonalDetails, LocationDetails, ContractorDetails, SavedDetails
13
  from local_storage.ls_manager import LocalStorageManager
14
  from prompts.prompts_manager import PromptsManager
15
+ from enums import Questions as Q, DetailsType
16
  from repository.repository import build_repo_from_environment, get_repository
17
  from repository import ModelRoles, Model
18
  from utils.parsing_utils import check_for_missing_answers
 
21
 
22
  find_tags_regex = re.compile(r"@(\S*)")
23
 
24
+
25
  class Steps(Enum):
26
  INITIAL_STATE = 1
27
  PARSING_ANSWERS = 2
 
47
  get_repository("testing",
48
  Model("fakeModel",
49
  ModelRoles("a", "b", "c"))))
50
+ self.update_in_progress = False
51
  self.lsm = None
52
+
53
  @staticmethod
54
  def get_current_step() -> int:
55
  try:
 
61
  def set_current_step(step: Steps):
62
  ss["step"] = step.value
63
 
64
+ def _build_base_ui(self):
 
65
  st.markdown("## Dubai Asset Management red tape cutter")
66
+ self.lsm = LocalStorageManager() if not self.lsm else self.lsm
67
+ with st.sidebar:
68
+ st.markdown("### Personal details")
69
+ self.build_details_checkboxes(DetailsType.PERSONAL_DETAILS)
70
+ st.markdown("### Locations details")
71
+ self.build_details_checkboxes(DetailsType.LOCATION_DETAILS)
72
+ st.markdown("### Contractors details")
73
+ self.build_details_checkboxes(DetailsType.CONTRACTOR_DETAILS)
74
 
75
  def build_ui_for_initial_state(self, user_message):
76
  help_ = user_message + "\n".join(self.pm.questions)
77
  self._build_base_ui()
78
  with st.form("Please describe your request"):
79
+ st.text_area("Your input", height=700, label_visibility="hidden", placeholder=help_,
80
+ help=help_, key="user_input")
81
+ signature = st.file_uploader("Your signature", key="file_upload")
82
+ ss["signature"] = signature
83
+ submit_button = st.form_submit_button()
84
+ if submit_button:
85
+ self.set_current_step(Steps.PARSING_ANSWERS)
86
+ st.rerun()
 
87
 
88
  def build_ui_for_parsing_answers(self):
89
  self._build_base_ui()
90
  with st.status("parsing user input for tags"):
 
91
  tags = find_tags_regex.findall(ss["user_input"])
92
  details = [self.lsm.get_detail(t) for t in tags]
93
  with st.status("initialising LLM"):
 
175
  self._build_validation_form(True, self._create_pdf_form,
176
  "Create work permit request")
177
 
 
178
  @staticmethod
179
  def _get_personal_details(personal_details_key) -> PersonalDetails | None:
180
  key_ = ss.get(personal_details_key)
181
  if key_:
182
+ details = PersonalDetails(ss[f"fq_{Q.FULL_NAME.name}"], ss[f"fq_{Q.YOUR_EMAIL.name}"],
183
+ ss[f"fq_{Q.CONTACT_NUMBER.name}"])
184
  return details
185
  return None
186
 
 
198
  ss[f"fq_{Q.COMPANY_EMAIL.name}"])
199
  return None
200
 
201
+ def _build_validation_form(self, show_categories: bool, onsubmit, submit_button_label):
 
 
202
  def build_form_fragment(form_, col, title, add_save, *questions):
203
  form_.text(title)
204
  for user_data in questions:
 
214
  col1, col2 = f.columns(2)
215
  build_form_fragment(f, col1, "your details", True, Q.FULL_NAME, Q.CONTACT_NUMBER, Q.YOUR_EMAIL)
216
  build_form_fragment(f, col2, "work details", False, Q.WORK_TO_DO, Q.START_DATE, Q.END_DATE)
217
+ build_form_fragment(f, col1, "location details", True, Q.COMMUNITY, Q.BUILDING, Q.UNIT_APT_NUMBER,
218
  Q.OWNER_OR_TENANT)
219
  build_form_fragment(f, col2, "contractor details", True, Q.COMPANY_NAME, Q.COMPANY_NUMBER, Q.COMPANY_EMAIL)
220
  if show_categories:
 
225
  if submit_data:
226
  onsubmit()
227
 
228
+ def build_details_checkboxes(self, dt: DetailsType):
229
+ details = self.lsm.get_details(dt)
230
+ with st.container(border=True):
231
+ col1, col2 = st.columns(2)
232
+ with col1:
233
+ st.markdown(f"#### {dt.title()}")
234
+ with col2:
235
+ st.markdown("#### Default")
236
+ for d in details:
237
+ with col1:
238
+ st.checkbox(label=d.short_description(), key=f"{dt.name}_{d.key}",
239
+ on_change=self._update_user_prompt, args=[dt, d.key])
240
+ with col2:
241
+ st.toggle(f"favourite_{d.key}", label_visibility="hidden", value=ss.get(f"DEFAULT_{dt.name}"))
242
+ add_new = st.button(f"Add {dt.title()}")
243
+ if add_new:
244
+ self.add_new_detail_dialog(dt)
245
+
246
+ @st.dialog("Add new")
247
+ def add_new_detail_dialog(self, type_: DetailsType):
248
+ if type_.name == DetailsType.CONTRACTOR_DETAILS.name:
249
+ new_item = ContractorDetails()
250
+ elif type_.name == DetailsType.PERSONAL_DETAILS.name:
251
+ new_item = PersonalDetails()
252
+ else:
253
+ new_item = LocationDetails()
254
+ with st.form("new item", border=False):
255
+ fields_labels = new_item.widget_labels()
256
+ for k,v in fields_labels.items():
257
+ st.text_input(label=v, key=k)
258
+ btn_save = st.form_submit_button("Save")
259
+ if btn_save:
260
+ for k in fields_labels:
261
+ setattr(new_item, k, ss[k])
262
+ self.lsm.save_details(new_item)
263
+ st.rerun()
264
+ def _update_user_prompt(self, type_: DetailsType, key: str):
265
+ if not self.update_in_progress:
266
+ self.update_in_progress = True
267
+ checkbox_key = f"{type_.name}_{key}"
268
+ if ss.get(checkbox_key) is True:
269
+ # if the checkbox is _selected_
270
+ to_deselect = [d for d in self.lsm.get_details(type_) if d.key != key]
271
+ for td in to_deselect:
272
+ # deselect other checkbox in the same type
273
+ ss[f"{type_.name}_{td}"] = False
274
+ if f"@{td}" in ss.get("user_input"):
275
+ # remove the key associated with this checkbox from the user input textarea
276
+ ss["user_input"] = ss.get["user_input"].replace(f"@{td}", "")
277
+ # add the key associated to the newly selected checkbox in the user input textarea
278
+ ss["user_input"] = f"{ss.get('user_input', '')} @{key}".strip()
279
+ else:
280
+ # remove the key associated to the newly deselected checkbox in the user input textarea
281
+ if f"@{key}" in ss.get("user_input"):
282
+ ss["user_input"] = ss.get("user_input", "").replace(f"@{key}", "").strip()
283
+ self.update_in_progress = False
284
+
285
 
286
  um = UIManager()
287
 
 
288
 
289
+ def use_streamlit():
290
+ if um.get_current_step() == Steps.INITIAL_STATE.value:
291
  um.build_ui_for_initial_state(user_msg)
292
  elif um.get_current_step() == Steps.PARSING_ANSWERS.value:
293
  um.build_ui_for_parsing_answers()
enums.py CHANGED
@@ -1,16 +1,6 @@
1
  from enum import Enum
2
 
3
 
4
- class DetailsType(Enum):
5
- PERSONAL_DETAILS = 1
6
- LOCATION_DETAILS = 2
7
- CONTRACTOR_DETAILS = 3
8
-
9
- @classmethod
10
- def values(cls):
11
- return [cls.PERSONAL_DETAILS, cls.LOCATION_DETAILS, cls.CONTRACTOR_DETAILS]
12
-
13
-
14
  class Questions(Enum):
15
  FULL_NAME = 0
16
  WORK_TO_DO = 1
@@ -30,4 +20,17 @@ class Questions(Enum):
30
  def values(cls):
31
  return [Questions.FULL_NAME, Questions.WORK_TO_DO, Questions.BUILDING, Questions.UNIT_APT_NUMBER,
32
  Questions.OWNER_OR_TENANT, Questions.START_DATE, Questions.END_DATE, Questions.CONTACT_NUMBER,
33
- Questions.COMPANY_NAME, Questions.COMPANY_EMAIL, Questions.COMPANY_NUMBER, Questions.YOUR_EMAIL]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from enum import Enum
2
 
3
 
 
 
 
 
 
 
 
 
 
 
4
  class Questions(Enum):
5
  FULL_NAME = 0
6
  WORK_TO_DO = 1
 
20
  def values(cls):
21
  return [Questions.FULL_NAME, Questions.WORK_TO_DO, Questions.BUILDING, Questions.UNIT_APT_NUMBER,
22
  Questions.OWNER_OR_TENANT, Questions.START_DATE, Questions.END_DATE, Questions.CONTACT_NUMBER,
23
+ Questions.COMPANY_NAME, Questions.COMPANY_EMAIL, Questions.COMPANY_NUMBER, Questions.YOUR_EMAIL]
24
+
25
+
26
+ class DetailsType(Enum):
27
+ PERSONAL_DETAILS = 1
28
+ LOCATION_DETAILS = 2
29
+ CONTRACTOR_DETAILS = 3
30
+
31
+ def title(self):
32
+ return self.name.replace("_", " ").lower()
33
+
34
+ @classmethod
35
+ def values(cls):
36
+ return [cls.PERSONAL_DETAILS, cls.LOCATION_DETAILS, cls.CONTRACTOR_DETAILS]
local_storage/entities.py CHANGED
@@ -1,13 +1,14 @@
1
  import abc
2
 
3
- from enums import DetailsType, Questions
4
 
5
 
6
  class SavedDetails(abc.ABC):
7
-
8
  excluded_fields = ["type_"]
9
- def __init__(self, type_: DetailsType):
 
10
  self.type_ = type_
 
11
 
12
  @classmethod
13
  def load(cls, data: dict):
@@ -17,16 +18,26 @@ class SavedDetails(abc.ABC):
17
  return cls.__init__(**{k: v for k, v in data if k != "type"})
18
 
19
  def to_json(self):
20
- return {k:v for k,v in self.__dict__.items() if k not in self.excluded_fields}
 
21
 
22
  def to_answers(self):
23
  pass
24
 
 
 
 
 
 
 
 
 
 
 
25
  class PersonalDetails(SavedDetails):
26
- type_ = DetailsType.PERSONAL_DETAILS
27
 
28
- def __init__(self, full_name, email, contact_number, *_):
29
- super().__init__(self.type_)
30
  self.full_name = full_name
31
  self.email = email
32
  self.contact_number = contact_number
@@ -41,11 +52,13 @@ class PersonalDetails(SavedDetails):
41
  answers[Questions.CONTACT_NUMBER] = self.contact_number
42
  return answers
43
 
 
44
  class LocationDetails(SavedDetails):
45
  type_ = DetailsType.LOCATION_DETAILS
46
 
47
- def __init__(self, owner_or_tenant: str, community: str, building: str, unit_number: str, *_):
48
- super().__init__(self.type_)
 
49
  self.owner_or_tenant = owner_or_tenant
50
  self.community = community
51
  self.building = building
@@ -63,11 +76,13 @@ class LocationDetails(SavedDetails):
63
  answers[Questions.UNIT_APT_NUMBER] = self.unit_number
64
  return answers
65
 
 
66
  class ContractorDetails(SavedDetails):
67
  type_ = DetailsType.CONTRACTOR_DETAILS
68
 
69
- def __init__(self, contractor_name: str, contractor_contact_number: str, contractor_email: str, *_):
70
- super().__init__(self.type_)
 
71
  self.contractor_name = contractor_name
72
  self.contractor_contact_number = contractor_contact_number
73
  self.contractor_email = contractor_email
@@ -80,4 +95,4 @@ class ContractorDetails(SavedDetails):
80
  answers[Questions.COMPANY_NUMBER] = self.contractor_contact_number
81
  if self.contractor_name is not None:
82
  answers[Questions.COMPANY_NAME] = self.contractor_name
83
- return answers
 
1
  import abc
2
 
3
+ from enums import Questions, DetailsType
4
 
5
 
6
  class SavedDetails(abc.ABC):
 
7
  excluded_fields = ["type_"]
8
+
9
+ def __init__(self, type_: DetailsType, key: str = None):
10
  self.type_ = type_
11
+ self.key = key
12
 
13
  @classmethod
14
  def load(cls, data: dict):
 
18
  return cls.__init__(**{k: v for k, v in data if k != "type"})
19
 
20
  def to_json(self):
21
+ self.key = self.key.replace(" ", "_")
22
+ return {k: v for k, v in self.__dict__.items() if k not in self.excluded_fields}
23
 
24
  def to_answers(self):
25
  pass
26
 
27
+ def short_description(self):
28
+ return ", ".join([v for k, v in self.__dict__.items() if k not in self.excluded_fields])
29
+
30
+ def widget_labels(self) -> dict[str, str]:
31
+ fields = {k: " ".join(k.title().split("_")) for k in self.__dict__
32
+ if k not in self.excluded_fields and k != "key"}
33
+ fields["key"] = "Save as"
34
+ return fields
35
+
36
+
37
  class PersonalDetails(SavedDetails):
 
38
 
39
+ def __init__(self, key: str = None, full_name: str = None, email: str = None, contact_number: str = None, *_):
40
+ super().__init__(DetailsType.PERSONAL_DETAILS, key)
41
  self.full_name = full_name
42
  self.email = email
43
  self.contact_number = contact_number
 
52
  answers[Questions.CONTACT_NUMBER] = self.contact_number
53
  return answers
54
 
55
+
56
  class LocationDetails(SavedDetails):
57
  type_ = DetailsType.LOCATION_DETAILS
58
 
59
+ def __init__(self, owner_or_tenant: str = None, community: str = None, building: str = None,
60
+ unit_number: str = None, key: str = None, *_):
61
+ super().__init__(self.type_, key)
62
  self.owner_or_tenant = owner_or_tenant
63
  self.community = community
64
  self.building = building
 
76
  answers[Questions.UNIT_APT_NUMBER] = self.unit_number
77
  return answers
78
 
79
+
80
  class ContractorDetails(SavedDetails):
81
  type_ = DetailsType.CONTRACTOR_DETAILS
82
 
83
+ def __init__(self, contractor_name: str = None, contractor_contact_number: str = None, contractor_email: str = None,
84
+ key: str = None, *_):
85
+ super().__init__(self.type_, key)
86
  self.contractor_name = contractor_name
87
  self.contractor_contact_number = contractor_contact_number
88
  self.contractor_email = contractor_email
 
95
  answers[Questions.COMPANY_NUMBER] = self.contractor_contact_number
96
  if self.contractor_name is not None:
97
  answers[Questions.COMPANY_NAME] = self.contractor_name
98
+ return answers
local_storage/ls_manager.py CHANGED
@@ -8,7 +8,7 @@ class LocalStorageManager:
8
  def __init__(self):
9
  self.ls: LocalStorage = LocalStorage()
10
 
11
- def get_detail(self, key: str):
12
  for detail_type in DetailsType.values():
13
  detail = self.ls.getItem(detail_type.name).get(key)
14
  if detail:
@@ -21,7 +21,7 @@ class LocalStorageManager:
21
  return None
22
  return None
23
 
24
- def save_details(self, details: SavedDetails, key: str):
25
  if isinstance(details, PersonalDetails):
26
  type_ = DetailsType.PERSONAL_DETAILS
27
  elif isinstance(details, LocationDetails):
@@ -31,8 +31,15 @@ class LocalStorageManager:
31
  else:
32
  raise ValueError("Unexpected type: {}", type(details))
33
  existing_data = (self.ls.getItem(type_.name) or {})
34
- existing_data[key] = details.to_json()
35
  self.ls.setItem(type_.name, existing_data, type_.name)
36
 
37
- def get_details(self, type_: DetailsType):
38
- return self.ls.getItem(type_.name) or {}
 
 
 
 
 
 
 
 
8
  def __init__(self):
9
  self.ls: LocalStorage = LocalStorage()
10
 
11
+ def get_detail(self, key: str = None):
12
  for detail_type in DetailsType.values():
13
  detail = self.ls.getItem(detail_type.name).get(key)
14
  if detail:
 
21
  return None
22
  return None
23
 
24
+ def save_details(self, details: SavedDetails):
25
  if isinstance(details, PersonalDetails):
26
  type_ = DetailsType.PERSONAL_DETAILS
27
  elif isinstance(details, LocationDetails):
 
31
  else:
32
  raise ValueError("Unexpected type: {}", type(details))
33
  existing_data = (self.ls.getItem(type_.name) or {})
34
+ existing_data[details.key] = details.to_json()
35
  self.ls.setItem(type_.name, existing_data, type_.name)
36
 
37
+ def get_details(self, type_: DetailsType) -> list[SavedDetails]:
38
+ if type_.value == DetailsType.PERSONAL_DETAILS.value:
39
+ cls = PersonalDetails
40
+ elif type_.value == DetailsType.LOCATION_DETAILS.value:
41
+ cls = LocationDetails
42
+ else:
43
+ cls = ContractorDetails
44
+ details = self.ls.getItem(type_.name) or {}
45
+ return [cls(**v) for k,v in details.items()]
requirements-base.txt CHANGED
@@ -1,4 +1,5 @@
1
  transformers
2
  streamlit
3
  PyPDFForm
4
- streamlit-local-storage
 
 
1
  transformers
2
  streamlit
3
  PyPDFForm
4
+ streamlit-local-storage
5
+ streamlit-tags