enricorampazzo commited on
Commit
c1c7334
·
1 Parent(s): ebaa573

now creates email with attachment

Browse files
app.py CHANGED
@@ -1,292 +1,10 @@
1
- import re
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
19
 
20
  user_msg = "Please describe what you need to do. To get the best results try to answer all the following questions:"
21
 
22
- find_tags_regex = re.compile(r"@(\S*)")
23
-
24
-
25
- class Steps(Enum):
26
- INITIAL_STATE = 1
27
- PARSING_ANSWERS = 2
28
- ASK_AGAIN = 3
29
- FIND_CATEGORIES = 4
30
- VALIDATE_DATA = 5
31
- PARSING_ERROR = 6
32
- FORM_CREATED = 7
33
-
34
- def __eq__(self, other):
35
- if not isinstance(other, self.__class__):
36
- return False
37
- return self.value == other.value
38
-
39
- def __hash__(self):
40
- return hash(self.value)
41
-
42
-
43
- class UIManager:
44
- def __init__(self):
45
- self.pm: PromptsManager = PromptsManager(work_categories=work_categories)
46
- self.repository = (build_repo_from_environment(self.pm.system_prompt) or
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:
56
- return ss.get("step") or Steps.INITIAL_STATE.value
57
- except StopException:
58
- return Steps.INITIAL_STATE.value
59
-
60
- @staticmethod
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"):
94
- self.repository.init()
95
- with st.status("waiting for LLM"):
96
- answer = self.repository.send_prompt(self.pm.verify_user_input_prompt(ss["user_input"], details))
97
- st.write(f"answers from LLM: {answer['content']}")
98
- with st.status("Checking for missing answers"):
99
- answers = LlmParser.parse_verification_prompt_answers(answer['content'], details)
100
- ss["answers"] = answers
101
- if len(answers) != len(Q):
102
- self.set_current_step(Steps.PARSING_ERROR)
103
- st.rerun()
104
- ss["missing_answers"] = check_for_missing_answers(ss["answers"])
105
- if not ss.get("missing_answers"):
106
- self.set_current_step(Steps.FIND_CATEGORIES)
107
- else:
108
- self.set_current_step(Steps.ASK_AGAIN)
109
- st.rerun()
110
-
111
- def build_ui_for_ask_again(self):
112
- self._build_base_ui()
113
- with st.form("form1"):
114
- for ma in ss["missing_answers"]:
115
- st.text_input(self.pm.questions[ma.value].lower(), key=ma)
116
- submitted = st.form_submit_button("Submit answers")
117
- if submitted:
118
- for ma in ss["missing_answers"]:
119
- ss["answers"][ma] = ss[ma]
120
- self.set_current_step(Steps.FIND_CATEGORIES)
121
- st.rerun()
122
-
123
- def build_ui_for_check_category(self):
124
- self._build_base_ui()
125
- with st.status("finding the work categories applicable to your work"):
126
- answer = self.repository.send_prompt(self.pm.get_work_category(ss["answers"][Q.WORK_TO_DO]))
127
- categories = LlmParser.parse_get_categories_answer(answer['content'])
128
- ss["categories"] = categories
129
- self.set_current_step(Steps.VALIDATE_DATA)
130
- st.rerun()
131
-
132
- def build_ui_for_form_created(self):
133
- self._build_base_ui()
134
- st.download_button("download form", ss["pdf_form"],
135
- file_name=ss["pdf_form_filename"], mime="application/pdf")
136
- start_over_button = st.button("Start over")
137
- if start_over_button:
138
- del ss["step"]
139
- del ss["pdf_form"]
140
- del ss["pdf_form_filename"]
141
- if "signature" in ss:
142
- del ss["signature"]
143
- st.rerun()
144
-
145
- def _integrate_llm_answers_with_user_corrections(self):
146
- for i in range(len(Q)):
147
- ss["answers"][Q(i)] = ss[f"fq_{Q(i).name}"]
148
-
149
- for details_key, func in [("your_details", self._get_personal_details),
150
- ("location_details", self._get_location_details),
151
- ("contractor_details", self._get_contractor_details)]:
152
- details = func(details_key)
153
- if details:
154
- key = ss[details_key] # get the name under which this data should be saved
155
- self.lsm.save_details(details, key)
156
- self.set_current_step(Steps.FIND_CATEGORIES)
157
- st.rerun()
158
-
159
- def _create_pdf_form(self):
160
- with st.status("categories found, creating PDF form"):
161
- form_data, filename = build_form_data_from_answers(ss["answers"], ss["categories"],
162
- ss.get("signature"))
163
- pdf_form = write_pdf_form(form_data)
164
- pdf_form_filename = filename
165
- ss["pdf_form"] = pdf_form
166
- ss["pdf_form_filename"] = pdf_form_filename
167
- self.set_current_step(Steps.FORM_CREATED)
168
- st.rerun()
169
-
170
- def build_ui_for_validate_data_after_correction(self):
171
- self._build_validation_form(False, self._integrate_llm_answers_with_user_corrections,
172
- "Find work categories")
173
-
174
- def build_ui_to_confirm_form_data(self):
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
-
187
- @staticmethod
188
- def _get_location_details(location_details_key) -> LocationDetails | None:
189
- if ss.get(location_details_key):
190
- return LocationDetails(ss[f"fq_{Q.OWNER_OR_TENANT.name}"], ss[f"fq_{Q.COMMUNITY.name}"],
191
- ss[f"fq_{Q.BUILDING.name}"], ss[f"fq_{Q.UNIT_APT_NUMBER.name}"])
192
- return None
193
-
194
- @staticmethod
195
- def _get_contractor_details(contractor_details_key) -> ContractorDetails | None:
196
- if ss.get(contractor_details_key):
197
- return ContractorDetails(ss[f"fq_{Q.COMPANY_NAME.name}"], ss[f"fq_{Q.COMPANY_NUMBER.name}"],
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:
205
- with col:
206
- form_.text_input(self.pm.questions_to_field_labels()[user_data], value=ss.get("answers", {})
207
- .get(user_data), key=f"fq_{user_data.name}")
208
- if add_save:
209
- with col:
210
- form_.text_input("Save as", key=title.replace(" ", "_"))
211
-
212
- self._build_base_ui()
213
- f = st.form("Please check the following information and correct fix any inaccuracies")
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:
221
- for k, wc in work_categories.items():
222
- f.checkbox(label=wc, key=k, value=k in ss["categories"])
223
-
224
- submit_data = f.form_submit_button(label=submit_button_label)
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:
@@ -303,4 +21,4 @@ def use_streamlit():
303
  um.build_ui_for_form_created()
304
 
305
 
306
- use_streamlit()
 
1
+ from enums.enums import Steps
2
+ from ui_manager.ui_manager import UIManager
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  user_msg = "Please describe what you need to do. To get the best results try to answer all the following questions:"
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ def use_streamlit(um: UIManager):
8
  if um.get_current_step() == Steps.INITIAL_STATE.value:
9
  um.build_ui_for_initial_state(user_msg)
10
  elif um.get_current_step() == Steps.PARSING_ANSWERS.value:
 
21
  um.build_ui_for_form_created()
22
 
23
 
24
+ use_streamlit(UIManager())
email_manager/__init__.py ADDED
File without changes
email_manager/email_manager.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from email.message import EmailMessage
2
+
3
+ from enums.enums import Questions
4
+ from prompts.prompts_manager import PromptsManager
5
+ from repository import Repository
6
+
7
+
8
+ class EmailManager:
9
+
10
+ def __init__(self, pm: PromptsManager, repository:Repository):
11
+ self.pm = pm
12
+ self.repository = repository
13
+
14
+ def create_email(self, answers: dict[Questions, str], pdf_form, filename) -> EmailMessage:
15
+ msg = EmailMessage()
16
+ msg["Subject"] = (f"Minor work permit for unit {answers[Questions.UNIT_APT_NUMBER]} in "
17
+ f"{answers[Questions.COMMUNITY]}")
18
+ msg["From"] = answers[Questions.YOUR_EMAIL]
19
+ msg["To"] = "[email protected]"
20
+ msg.set_content(self.repository.send_prompt(self.pm.compose_email(answers), True)['content'])
21
+ msg.add_attachment(pdf_form, maintype="application", subtype="pdf", filename=filename)
22
+ return msg
enums/__init__.py ADDED
File without changes
enums.py → enums/enums.py RENAMED
@@ -34,3 +34,13 @@ class DetailsType(Enum):
34
  @classmethod
35
  def values(cls):
36
  return [cls.PERSONAL_DETAILS, cls.LOCATION_DETAILS, cls.CONTRACTOR_DETAILS]
 
 
 
 
 
 
 
 
 
 
 
34
  @classmethod
35
  def values(cls):
36
  return [cls.PERSONAL_DETAILS, cls.LOCATION_DETAILS, cls.CONTRACTOR_DETAILS]
37
+
38
+
39
+ class Steps(Enum):
40
+ INITIAL_STATE = 1
41
+ PARSING_ANSWERS = 2
42
+ ASK_AGAIN = 3
43
+ FIND_CATEGORIES = 4
44
+ VALIDATE_DATA = 5
45
+ PARSING_ERROR = 6
46
+ FORM_CREATED = 7
form/form.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  from typing import TypedDict, Tuple
3
  from PyPDFForm import PdfWrapper
4
 
5
- from enums import Questions
6
  from utils.date_utils import get_today_date_as_dd_mm_yyyy
7
  from utils.parsing_utils import find_and_parse_date, find_and_parse_phone_number
8
 
 
2
  from typing import TypedDict, Tuple
3
  from PyPDFForm import PdfWrapper
4
 
5
+ from enums.enums import Questions
6
  from utils.date_utils import get_today_date_as_dd_mm_yyyy
7
  from utils.parsing_utils import find_and_parse_date, find_and_parse_phone_number
8
 
llm_manager/llm_parser.py CHANGED
@@ -1,6 +1,6 @@
1
  from form.form import work_categories
2
  from local_storage.entities import SavedDetails
3
- from enums import Questions
4
 
5
 
6
  class LlmParser:
 
1
  from form.form import work_categories
2
  from local_storage.entities import SavedDetails
3
+ from enums.enums import Questions
4
 
5
 
6
  class LlmParser:
local_storage/entities.py CHANGED
@@ -1,6 +1,6 @@
1
  import abc
2
 
3
- from enums import Questions, DetailsType
4
 
5
 
6
  class SavedDetails(abc.ABC):
 
1
  import abc
2
 
3
+ from enums.enums import Questions, DetailsType
4
 
5
 
6
  class SavedDetails(abc.ABC):
local_storage/ls_manager.py CHANGED
@@ -1,7 +1,7 @@
1
  from streamlit_local_storage import LocalStorage
2
 
3
  from local_storage.entities import SavedDetails, PersonalDetails, LocationDetails, ContractorDetails
4
- from enums import DetailsType
5
 
6
 
7
  class LocalStorageManager:
 
1
  from streamlit_local_storage import LocalStorage
2
 
3
  from local_storage.entities import SavedDetails, PersonalDetails, LocationDetails, ContractorDetails
4
+ from enums.enums import DetailsType
5
 
6
 
7
  class LocalStorageManager:
prompts/prompts_manager.py CHANGED
@@ -1,7 +1,7 @@
1
  from pathlib import Path
2
 
3
- from enums import Questions
4
- from local_storage.entities import SavedDetails, PersonalDetails, ContractorDetails, LocationDetails
5
  from utils.date_utils import get_today_date_as_dd_mm_yyyy
6
 
7
 
@@ -20,14 +20,14 @@ class PromptsManager:
20
  self.verification_prompt: str = verification_prompt
21
  self.verification_prompt_questions = self.verification_prompt.split("\n")
22
 
23
- def verify_user_input_prompt(self, user_prompt, exclude_questions_group: list[SavedDetails] = None) -> str:
24
  prompt = f"""
25
  Using only this information \n {user_prompt} \n answer the following questions, for each question that you cannot answer just answer 'null'.
26
  Put each answer in a new line, keep the answer brief
27
  and maintain the order in which the questions are asked. Do not add any preamble:
28
  """
29
 
30
- skip_questions = self.get_questions(exclude_questions_group)
31
  questions = [q for idx, q in enumerate(self.verification_prompt_questions) if idx not in skip_questions]
32
  return prompt + "\n".join(questions)
33
 
@@ -48,8 +48,24 @@ class PromptsManager:
48
  Questions.COMPANY_NUMBER: "Contracting company contact number", Questions.YOUR_EMAIL: "Your email"
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  @staticmethod
52
- def get_questions(details: list[int]):
53
  to_skip: list[int] = []
54
  for d in details:
55
  if isinstance(d, PersonalDetails):
 
1
  from pathlib import Path
2
 
3
+ from enums.enums import Questions
4
+ from local_storage.entities import PersonalDetails, ContractorDetails, LocationDetails
5
  from utils.date_utils import get_today_date_as_dd_mm_yyyy
6
 
7
 
 
20
  self.verification_prompt: str = verification_prompt
21
  self.verification_prompt_questions = self.verification_prompt.split("\n")
22
 
23
+ def verify_user_input_prompt(self, user_prompt, exclude_questions_group: list[int] = None) -> str:
24
  prompt = f"""
25
  Using only this information \n {user_prompt} \n answer the following questions, for each question that you cannot answer just answer 'null'.
26
  Put each answer in a new line, keep the answer brief
27
  and maintain the order in which the questions are asked. Do not add any preamble:
28
  """
29
 
30
+ skip_questions = self._get_questions(exclude_questions_group)
31
  questions = [q for idx, q in enumerate(self.verification_prompt_questions) if idx not in skip_questions]
32
  return prompt + "\n".join(questions)
33
 
 
48
  Questions.COMPANY_NUMBER: "Contracting company contact number", Questions.YOUR_EMAIL: "Your email"
49
  }
50
 
51
+ def compose_email(self, answers: dict[Questions, str]):
52
+ prompt = f"""
53
+ Compose a formal email directed at Dubai Community Management asking for a minor work permit.
54
+ It should include the following details:
55
+ My name is {answers[Questions.FULL_NAME]} and I am the {answers[Questions.OWNER_OR_TENANT]} of unit
56
+ {answers[Questions.UNIT_APT_NUMBER]} located in {answers[Questions.BUILDING]}, {answers[Questions.COMMUNITY]};
57
+
58
+ The work involves {answers[Questions.WORK_TO_DO]}, and will be carried out by {answers[Questions.COMPANY_NAME]}
59
+ from {answers[Questions.START_DATE]} to {answers[Questions.END_DATE]};
60
+
61
+ The work permit request form is attached.
62
+
63
+ Please return only the email body, without any preamble
64
+ """
65
+ return prompt
66
+
67
  @staticmethod
68
+ def _get_questions(details: list[int]):
69
  to_skip: list[int] = []
70
  for d in details:
71
  if isinstance(d, PersonalDetails):
ui_manager/__init__.py ADDED
File without changes
ui_manager.py → ui_manager/ui_manager.py RENAMED
@@ -1,108 +1,122 @@
1
- import json
2
- import os
3
  import streamlit as st
4
  from streamlit import session_state as ss
5
- from streamlit_local_storage import LocalStorage
6
 
7
- from form.form import build_form_data_from_answers, write_pdf_form
 
 
8
  from llm_manager.llm_parser import LlmParser
9
  from local_storage.entities import PersonalDetails, LocationDetails, ContractorDetails
 
10
  from prompts.prompts_manager import PromptsManager
11
- from enums import Questions as Q
12
- from repository.repository import get_repository
13
- from repository import ModelRoles, Model
14
  from utils.parsing_utils import check_for_missing_answers
15
 
16
- ls: LocalStorage = LocalStorage()
17
-
18
- def in_hf() -> bool:
19
- return os.getenv("env") == "hf"
20
-
21
-
22
-
23
 
24
  class UIManager:
25
  def __init__(self):
26
- self.pm: PromptsManager = PromptsManager()
 
27
  self.repository = (build_repo_from_environment(self.pm.system_prompt) or
28
  get_repository("testing",
29
- Model("fakeModel", ModelRoles("a", "b", "c"))))
 
 
 
 
30
 
31
  @staticmethod
32
- def get_current_step():
33
- return ss.get("step")
 
 
 
34
 
35
  @staticmethod
36
- def _build_base_ui():
 
 
 
37
  st.markdown("## Dubai Asset Management red tape cutter")
 
 
 
 
 
 
 
 
38
 
39
  def build_ui_for_initial_state(self, user_message):
40
- help_ = user_message
41
  self._build_base_ui()
42
  with st.form("Please describe your request"):
43
- user_input = st.text_area("Your input", height=700, label_visibility="hidden", placeholder=help_,
44
- help=help_)
45
  signature = st.file_uploader("Your signature", key="file_upload")
46
  ss["signature"] = signature
47
  submit_button = st.form_submit_button()
48
  if submit_button:
49
- ss["user_input"] = user_input
50
- ss["step"] = "parsing_answers"
51
  st.rerun()
52
 
53
  def build_ui_for_parsing_answers(self):
54
  self._build_base_ui()
 
 
 
55
  with st.status("initialising LLM"):
56
  self.repository.init()
57
  with st.status("waiting for LLM"):
58
- answer = self.repository.send_prompt(self.pm.verify_user_input_prompt(ss["user_input"]))
59
  st.write(f"answers from LLM: {answer['content']}")
60
  with st.status("Checking for missing answers"):
61
- answers = LlmParser.parse_verification_prompt_answers(answer['content'])
62
  ss["answers"] = answers
63
  if len(answers) != len(Q):
64
- ss["step"] = "parsing_error"
65
  st.rerun()
66
  ss["missing_answers"] = check_for_missing_answers(ss["answers"])
67
  if not ss.get("missing_answers"):
68
- ss["step"] = "check_category"
69
  else:
70
- ss["step"] = "ask_again"
71
  st.rerun()
72
 
73
  def build_ui_for_ask_again(self):
74
  self._build_base_ui()
75
  with st.form("form1"):
76
  for ma in ss["missing_answers"]:
77
- st.text_input(self.pm.questions[ma].lower(), key=ma)
78
  submitted = st.form_submit_button("Submit answers")
79
  if submitted:
80
  for ma in ss["missing_answers"]:
81
  ss["answers"][ma] = ss[ma]
82
- ss["step"] = "check_category"
83
  st.rerun()
84
 
85
  def build_ui_for_check_category(self):
86
  self._build_base_ui()
87
  with st.status("finding the work categories applicable to your work"):
88
- answer = self.repository.send_prompt(self.pm.get_work_category(ss["answers"][1]))
89
  categories = LlmParser.parse_get_categories_answer(answer['content'])
90
-
91
- with st.status("categories found, creating PDF form"):
92
- form_data, filename = build_form_data_from_answers(ss["answers"], categories,
93
- ss.get("signature"))
94
- pdf_form = write_pdf_form(form_data)
95
- pdf_form_filename = filename
96
- ss["pdf_form"] = pdf_form
97
- ss["pdf_form_filename"] = pdf_form_filename
98
- ss["step"] = "form_created"
99
  st.rerun()
100
 
101
  def build_ui_for_form_created(self):
102
  self._build_base_ui()
103
  st.download_button("download form", ss["pdf_form"],
104
  file_name=ss["pdf_form_filename"], mime="application/pdf")
 
 
 
 
105
  start_over_button = st.button("Start over")
 
106
  if start_over_button:
107
  del ss["step"]
108
  del ss["pdf_form"]
@@ -111,53 +125,143 @@ class UIManager:
111
  del ss["signature"]
112
  st.rerun()
113
 
114
- def build_ui_for_parsing_error(self):
115
- def build_form_fragment(form_, col, title, *questions):
116
- form_.text(title)
117
- for user_data in questions:
118
- with col:
119
- form_.text_input(self.pm.questions_to_field_labels()[user_data], value=ss.get("answers", {})
120
- .get(user_data), key=f"fq_{user_data.value}")
121
- with col:
122
- form_.text_input("Save as", key=title.replace(" ", "_"))
123
 
124
- self._build_base_ui()
125
- f = st.form("Please check the following information and correct fix any inaccuracies")
126
- col1, col2 = f.columns(2)
127
- build_form_fragment(f, col1, "your details", Q.FULL_NAME, Q.CONTACT_NUMBER, Q.YOUR_EMAIL)
128
- build_form_fragment(f, col2, "work details", Q.WORK_TO_DO, Q.START_DATE, Q.END_DATE)
129
- build_form_fragment(f, col1, "location details", Q.COMMUNITY, Q.BUILDING, Q.UNIT_APT_NUMBER,
130
- Q.OWNER_OR_TENANT)
131
- build_form_fragment(f, col2, "contractor details", Q.COMPANY_NAME, Q.COMPANY_NUMBER, Q.COMPANY_EMAIL)
132
- submit_data = f.form_submit_button()
133
- if submit_data:
134
- for i in range(len(Q)):
135
- ss["answers"][Q(i)] = ss[f"fq_{i}"]
136
 
137
- for details_key, func in [("your_details", self._get_personal_details),
138
- ("location_details", self._get_location_details),
139
- ("contractor_details", self._get_contractor_details)]:
140
- details = func(details_key)
141
- if details:
142
- key = ss[details_key] # get the name under which this data should be saved
143
- ls.setItem(key, json.dumps(details))
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  @staticmethod
146
  def _get_personal_details(personal_details_key) -> PersonalDetails | None:
147
- if ss.get(personal_details_key):
148
- return PersonalDetails(ss[f"fq_{Q.FULL_NAME}"], ss[f"fq_{Q.FULL_NAME}"], ss[f"fq_{Q.CONTACT_NUMBER}"])
 
 
 
149
  return None
150
 
151
  @staticmethod
152
  def _get_location_details(location_details_key) -> LocationDetails | None:
153
  if ss.get(location_details_key):
154
- return LocationDetails(ss[f"fq_{Q.OWNER_OR_TENANT}"], ss[f"fq_{Q.COMMUNITY}"], ss[f"fq_{Q.BUILDING}"],
155
- ss[f"fq_{Q.UNIT_APT_NUMBER}"])
156
  return None
157
 
158
  @staticmethod
159
  def _get_contractor_details(contractor_details_key) -> ContractorDetails | None:
160
  if ss.get(contractor_details_key):
161
- return ContractorDetails(ss[f"fq_{Q.COMPANY_NAME}"], ss[f"fq_{Q.COMPANY_NUMBER}"],
162
- ss[f"fq_{Q.COMPANY_EMAIL}"])
163
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
  import streamlit as st
4
  from streamlit import session_state as ss
5
+ from streamlit.runtime.scriptrunner_utils.exceptions import StopException
6
 
7
+ from email_manager.email_manager import EmailManager
8
+ from enums.enums import Steps, DetailsType, Questions as Q
9
+ from form.form import work_categories, build_form_data_from_answers, write_pdf_form
10
  from llm_manager.llm_parser import LlmParser
11
  from local_storage.entities import PersonalDetails, LocationDetails, ContractorDetails
12
+ from local_storage.ls_manager import LocalStorageManager
13
  from prompts.prompts_manager import PromptsManager
14
+ from repository import Model, ModelRoles
15
+ from repository.repository import build_repo_from_environment, get_repository
 
16
  from utils.parsing_utils import check_for_missing_answers
17
 
 
 
 
 
 
 
 
18
 
19
  class UIManager:
20
  def __init__(self):
21
+ self.find_tags_regex = re.compile(r"@(\S*)")
22
+ self.pm: PromptsManager = PromptsManager(work_categories=work_categories)
23
  self.repository = (build_repo_from_environment(self.pm.system_prompt) or
24
  get_repository("testing",
25
+ Model("fakeModel",
26
+ ModelRoles("a", "b", "c"))))
27
+ self.update_in_progress = False
28
+ self.lsm = None
29
+ self.em = EmailManager(self.pm, self.repository)
30
 
31
  @staticmethod
32
+ def get_current_step() -> int:
33
+ try:
34
+ return ss.get("step") or Steps.INITIAL_STATE.value
35
+ except StopException:
36
+ return Steps.INITIAL_STATE.value
37
 
38
  @staticmethod
39
+ def set_current_step(step: Steps):
40
+ ss["step"] = step.value
41
+
42
+ def _build_base_ui(self):
43
  st.markdown("## Dubai Asset Management red tape cutter")
44
+ self.lsm = LocalStorageManager() if not self.lsm else self.lsm
45
+ with st.sidebar:
46
+ st.markdown("### Personal details")
47
+ self.build_details_checkboxes(DetailsType.PERSONAL_DETAILS)
48
+ st.markdown("### Locations details")
49
+ self.build_details_checkboxes(DetailsType.LOCATION_DETAILS)
50
+ st.markdown("### Contractors details")
51
+ self.build_details_checkboxes(DetailsType.CONTRACTOR_DETAILS)
52
 
53
  def build_ui_for_initial_state(self, user_message):
54
+ help_ = user_message + "\n".join(self.pm.questions)
55
  self._build_base_ui()
56
  with st.form("Please describe your request"):
57
+ st.text_area("Your input", height=700, label_visibility="hidden", placeholder=help_,
58
+ help=help_, key="user_input")
59
  signature = st.file_uploader("Your signature", key="file_upload")
60
  ss["signature"] = signature
61
  submit_button = st.form_submit_button()
62
  if submit_button:
63
+ self.set_current_step(Steps.PARSING_ANSWERS)
 
64
  st.rerun()
65
 
66
  def build_ui_for_parsing_answers(self):
67
  self._build_base_ui()
68
+ with st.status("parsing user input for tags"):
69
+ tags = self.find_tags_regex.findall(ss["user_input"])
70
+ details = [self.lsm.get_detail(t) for t in tags]
71
  with st.status("initialising LLM"):
72
  self.repository.init()
73
  with st.status("waiting for LLM"):
74
+ answer = self.repository.send_prompt(self.pm.verify_user_input_prompt(ss["user_input"], details))
75
  st.write(f"answers from LLM: {answer['content']}")
76
  with st.status("Checking for missing answers"):
77
+ answers = LlmParser.parse_verification_prompt_answers(answer['content'], details)
78
  ss["answers"] = answers
79
  if len(answers) != len(Q):
80
+ self.set_current_step(Steps.PARSING_ERROR)
81
  st.rerun()
82
  ss["missing_answers"] = check_for_missing_answers(ss["answers"])
83
  if not ss.get("missing_answers"):
84
+ self.set_current_step(Steps.FIND_CATEGORIES)
85
  else:
86
+ self.set_current_step(Steps.ASK_AGAIN)
87
  st.rerun()
88
 
89
  def build_ui_for_ask_again(self):
90
  self._build_base_ui()
91
  with st.form("form1"):
92
  for ma in ss["missing_answers"]:
93
+ st.text_input(self.pm.questions[ma.value].lower(), key=ma)
94
  submitted = st.form_submit_button("Submit answers")
95
  if submitted:
96
  for ma in ss["missing_answers"]:
97
  ss["answers"][ma] = ss[ma]
98
+ self.set_current_step(Steps.FIND_CATEGORIES)
99
  st.rerun()
100
 
101
  def build_ui_for_check_category(self):
102
  self._build_base_ui()
103
  with st.status("finding the work categories applicable to your work"):
104
+ answer = self.repository.send_prompt(self.pm.get_work_category(ss["answers"][Q.WORK_TO_DO]))
105
  categories = LlmParser.parse_get_categories_answer(answer['content'])
106
+ ss["categories"] = categories
107
+ self.set_current_step(Steps.VALIDATE_DATA)
 
 
 
 
 
 
 
108
  st.rerun()
109
 
110
  def build_ui_for_form_created(self):
111
  self._build_base_ui()
112
  st.download_button("download form", ss["pdf_form"],
113
  file_name=ss["pdf_form_filename"], mime="application/pdf")
114
+
115
+ em = self.em.create_email(ss["answers"], ss["pdf_form"], ss["pdf_form_filename"])
116
+ st.download_button("download email with attached form", em.as_bytes(), file_name="work_permit_email.eml")
117
+
118
  start_over_button = st.button("Start over")
119
+
120
  if start_over_button:
121
  del ss["step"]
122
  del ss["pdf_form"]
 
125
  del ss["signature"]
126
  st.rerun()
127
 
128
+ def _integrate_llm_answers_with_user_corrections(self):
129
+ for i in range(len(Q)):
130
+ ss["answers"][Q(i)] = ss[f"fq_{Q(i).name}"]
 
 
 
 
 
 
131
 
132
+ for details_key, func in [("your_details", self._get_personal_details),
133
+ ("location_details", self._get_location_details),
134
+ ("contractor_details", self._get_contractor_details)]:
135
+ details = func(details_key)
136
+ if details:
137
+ key = ss[details_key] # get the name under which this data should be saved
138
+ self.lsm.save_details(details, key)
139
+ self.set_current_step(Steps.FIND_CATEGORIES)
140
+ st.rerun()
 
 
 
141
 
142
+ def _create_pdf_form(self):
143
+ with st.status("categories found, creating PDF form"):
144
+ form_data, filename = build_form_data_from_answers(ss["answers"], ss["categories"],
145
+ ss.get("signature"))
146
+ pdf_form = write_pdf_form(form_data)
147
+ pdf_form_filename = filename
148
+ ss["pdf_form"] = pdf_form
149
+ ss["pdf_form_filename"] = pdf_form_filename
150
+ self.set_current_step(Steps.FORM_CREATED)
151
+ st.rerun()
152
+
153
+ def build_ui_for_validate_data_after_correction(self):
154
+ self._build_validation_form(False, self._integrate_llm_answers_with_user_corrections,
155
+ "Find work categories")
156
+
157
+ def build_ui_to_confirm_form_data(self):
158
+ self._build_validation_form(True, self._create_pdf_form,
159
+ "Create work permit request")
160
 
161
  @staticmethod
162
  def _get_personal_details(personal_details_key) -> PersonalDetails | None:
163
+ key_ = ss.get(personal_details_key)
164
+ if key_:
165
+ details = PersonalDetails(ss[f"fq_{Q.FULL_NAME.name}"], ss[f"fq_{Q.YOUR_EMAIL.name}"],
166
+ ss[f"fq_{Q.CONTACT_NUMBER.name}"])
167
+ return details
168
  return None
169
 
170
  @staticmethod
171
  def _get_location_details(location_details_key) -> LocationDetails | None:
172
  if ss.get(location_details_key):
173
+ return LocationDetails(ss[f"fq_{Q.OWNER_OR_TENANT.name}"], ss[f"fq_{Q.COMMUNITY.name}"],
174
+ ss[f"fq_{Q.BUILDING.name}"], ss[f"fq_{Q.UNIT_APT_NUMBER.name}"])
175
  return None
176
 
177
  @staticmethod
178
  def _get_contractor_details(contractor_details_key) -> ContractorDetails | None:
179
  if ss.get(contractor_details_key):
180
+ return ContractorDetails(ss[f"fq_{Q.COMPANY_NAME.name}"], ss[f"fq_{Q.COMPANY_NUMBER.name}"],
181
+ ss[f"fq_{Q.COMPANY_EMAIL.name}"])
182
  return None
183
+
184
+ def _build_validation_form(self, show_categories: bool, on_submit, submit_button_label):
185
+ def build_form_fragment(form_, col, title, add_save, *questions):
186
+ form_.text(title)
187
+ for user_data in questions:
188
+ with col:
189
+ form_.text_input(self.pm.questions_to_field_labels()[user_data], value=ss.get("answers", {})
190
+ .get(user_data), key=f"fq_{user_data.name}")
191
+ if add_save:
192
+ with col:
193
+ form_.text_input("Save as", key=title.replace(" ", "_"))
194
+
195
+ self._build_base_ui()
196
+ f = st.form("Please check the following information and correct fix any inaccuracies")
197
+ col1, col2 = f.columns(2)
198
+ build_form_fragment(f, col1, "your details", True, Q.FULL_NAME, Q.CONTACT_NUMBER, Q.YOUR_EMAIL)
199
+ build_form_fragment(f, col2, "work details", False, Q.WORK_TO_DO, Q.START_DATE, Q.END_DATE)
200
+ build_form_fragment(f, col1, "location details", True, Q.COMMUNITY, Q.BUILDING, Q.UNIT_APT_NUMBER,
201
+ Q.OWNER_OR_TENANT)
202
+ build_form_fragment(f, col2, "contractor details", True, Q.COMPANY_NAME, Q.COMPANY_NUMBER, Q.COMPANY_EMAIL)
203
+ if show_categories:
204
+ for k, wc in work_categories.items():
205
+ f.checkbox(label=wc, key=k, value=k in ss["categories"])
206
+
207
+ submit_data = f.form_submit_button(label=submit_button_label)
208
+ if submit_data:
209
+ on_submit()
210
+
211
+ def build_details_checkboxes(self, dt: DetailsType):
212
+ details = self.lsm.get_details(dt)
213
+ with st.container(border=True):
214
+ col1, col2 = st.columns(2)
215
+ with col1:
216
+ st.markdown(f"#### {dt.title()}")
217
+ with col2:
218
+ st.markdown("#### Default")
219
+ for d in details:
220
+ with col1:
221
+ st.checkbox(label=d.short_description(), key=f"{dt.name}_{d.key}",
222
+ on_change=self._update_user_prompt, args=[dt, d.key])
223
+ with col2:
224
+ st.toggle(f"favourite_{d.key}", label_visibility="hidden", value=ss.get(f"DEFAULT_{dt.name}"))
225
+ add_new = st.button(f"Add {dt.title()}")
226
+ if add_new:
227
+ self.add_new_detail_dialog(dt)
228
+
229
+ @st.dialog("Add new")
230
+ def add_new_detail_dialog(self, type_: DetailsType):
231
+ if type_.name == DetailsType.CONTRACTOR_DETAILS.name:
232
+ new_item = ContractorDetails()
233
+ elif type_.name == DetailsType.PERSONAL_DETAILS.name:
234
+ new_item = PersonalDetails()
235
+ else:
236
+ new_item = LocationDetails()
237
+ with st.form("new item", border=False):
238
+ fields_labels = new_item.widget_labels()
239
+ for k, v in fields_labels.items():
240
+ st.text_input(label=v, key=k)
241
+ btn_save = st.form_submit_button("Save")
242
+ if btn_save:
243
+ for k in fields_labels:
244
+ setattr(new_item, k, ss[k])
245
+ self.lsm.save_details(new_item)
246
+ st.rerun()
247
+
248
+ def _update_user_prompt(self, type_: DetailsType, key: str):
249
+ if not self.update_in_progress:
250
+ self.update_in_progress = True
251
+ checkbox_key = f"{type_.name}_{key}"
252
+ if ss.get(checkbox_key) is True:
253
+ # if the checkbox is _selected_
254
+ to_deselect = [d for d in self.lsm.get_details(type_) if d.key != key]
255
+ for td in to_deselect:
256
+ # deselect other checkbox in the same type
257
+ ss[f"{type_.name}_{td}"] = False
258
+ if f"@{td}" in ss.get("user_input"):
259
+ # remove the key associated with this checkbox from the user input textarea
260
+ ss["user_input"] = ss.get["user_input"].replace(f"@{td}", "")
261
+ # add the key associated to the newly selected checkbox in the user input textarea
262
+ ss["user_input"] = f"{ss.get('user_input', '')} @{key}".strip()
263
+ else:
264
+ # remove the key associated to the newly deselected checkbox in the user input textarea
265
+ if f"@{key}" in ss.get("user_input"):
266
+ ss["user_input"] = ss.get("user_input", "").replace(f"@{key}", "").strip()
267
+ self.update_in_progress = False