enricorampazzo commited on
Commit
cb7ff7d
Β·
1 Parent(s): aa2cc5f

now saving personal, location and contractor details in the browser local storage correctly πŸ˜…

Browse files
app.py CHANGED
@@ -1,19 +1,21 @@
1
- import json
2
 
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, work_categories
8
  from llm_manager.llm_parser import LlmParser
9
  from local_storage.entities import PersonalDetails, LocationDetails, ContractorDetails
 
10
  from prompts.prompts_manager import PromptsManager, Questions as Q
11
  from repository.repository import build_repo_from_environment, get_repository
12
  from repository import ModelRoles, Model
13
  from utils.parsing_utils import check_for_missing_answers
14
 
15
  user_msg = "Please describe what you need to do. To get the best results try to answer all the following questions:"
16
- ls: LocalStorage = LocalStorage()
 
 
17
  class UIManager:
18
  def __init__(self):
19
  self.pm: PromptsManager = PromptsManager(work_categories=work_categories)
@@ -45,10 +47,14 @@ class UIManager:
45
 
46
  def build_ui_for_parsing_answers(self):
47
  self._build_base_ui()
 
 
 
 
48
  with st.status("initialising LLM"):
49
  self.repository.init()
50
  with st.status("waiting for LLM"):
51
- answer = self.repository.send_prompt(self.pm.verify_user_input_prompt(ss["user_input"]))
52
  st.write(f"answers from LLM: {answer['content']}")
53
  with st.status("Checking for missing answers"):
54
  answers = LlmParser.parse_verification_prompt_answers(answer['content'])
@@ -80,15 +86,8 @@ class UIManager:
80
  with st.status("finding the work categories applicable to your work"):
81
  answer = self.repository.send_prompt(self.pm.get_work_category(ss["answers"][Q.WORK_TO_DO]))
82
  categories = LlmParser.parse_get_categories_answer(answer['content'])
83
-
84
- with st.status("categories found, creating PDF form"):
85
- form_data, filename = build_form_data_from_answers(ss["answers"], categories,
86
- ss.get("signature"))
87
- pdf_form = write_pdf_form(form_data)
88
- pdf_form_filename = filename
89
- ss["pdf_form"] = pdf_form
90
- ss["pdf_form_filename"] = pdf_form_filename
91
- ss["step"] = "form_created"
92
  st.rerun()
93
 
94
  def build_ui_for_form_created(self):
@@ -104,44 +103,44 @@ class UIManager:
104
  del ss["signature"]
105
  st.rerun()
106
 
107
- def build_ui_for_parsing_error(self):
108
- def build_form_fragment(form_, col, title, add_save, *questions):
109
- form_.text(title)
110
- for user_data in questions:
111
- with col:
112
- form_.text_input(self.pm.questions_to_field_labels()[user_data], value=ss.get("answers", {})
113
- .get(user_data), key=f"fq_{user_data.name}")
114
- if add_save:
115
- with col:
116
- form_.text_input("Save as", key=title.replace(" ", "_"))
 
 
 
117
 
118
- self._build_base_ui()
119
- f = st.form("Please check the following information and correct fix any inaccuracies")
120
- col1, col2 = f.columns(2)
121
- build_form_fragment(f, col1, "your details", True, Q.FULL_NAME, Q.CONTACT_NUMBER, Q.YOUR_EMAIL)
122
- build_form_fragment(f, col2, "work details", False, Q.WORK_TO_DO, Q.START_DATE, Q.END_DATE)
123
- build_form_fragment(f, col1, "location details", True, Q.COMMUNITY, Q.BUILDING, Q.UNIT_APT_NUMBER,
124
- Q.OWNER_OR_TENANT)
125
- build_form_fragment(f, col2, "contractor details", True, Q.COMPANY_NAME, Q.COMPANY_NUMBER, Q.COMPANY_EMAIL)
126
- submit_data = f.form_submit_button()
127
- if submit_data:
128
- for i in range(len(Q)):
129
- ss["answers"][Q(i)] = ss[f"fq_{Q(i).name}"]
130
-
131
- for details_key, func in [("your_details", self._get_personal_details),
132
- ("location_details", self._get_location_details),
133
- ("contractor_details", self._get_contractor_details)]:
134
- details = func(details_key)
135
- if details:
136
- key = ss[details_key] # get the name under which this data should be saved
137
- ls.setItem(key, json.dumps(details.__dict__), key)
138
- ss["step"] = "check_category"
139
  st.rerun()
140
 
 
 
 
 
 
 
 
141
  @staticmethod
142
  def _get_personal_details(personal_details_key) -> PersonalDetails | None:
143
- if ss.get(personal_details_key):
144
- return PersonalDetails(ss[f"fq_{Q.FULL_NAME.name}"], ss[f"fq_{Q.YOUR_EMAIL.name}"], ss[f"fq_{Q.CONTACT_NUMBER.name}"])
 
 
145
  return None
146
 
147
  @staticmethod
@@ -159,6 +158,34 @@ class UIManager:
159
  return None
160
 
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  def use_streamlit():
163
 
164
  um = UIManager()
@@ -168,11 +195,13 @@ def use_streamlit():
168
  if um.get_current_step() == "parsing_answers":
169
  um.build_ui_for_parsing_answers()
170
  if um.get_current_step() == "parsing_error":
171
- um.build_ui_for_parsing_error()
172
  if um.get_current_step() == "ask_again":
173
  um.build_ui_for_ask_again()
174
  if um.get_current_step() == "check_category":
175
  um.build_ui_for_check_category()
 
 
176
  if um.get_current_step() == "form_created":
177
  um.build_ui_for_form_created()
178
 
 
1
+ import re
2
 
3
  import streamlit as st
4
  from streamlit import session_state as ss
 
5
 
6
  from form.form import build_form_data_from_answers, write_pdf_form, work_categories
7
  from llm_manager.llm_parser import LlmParser
8
  from local_storage.entities import PersonalDetails, LocationDetails, ContractorDetails
9
+ from local_storage.ls_manager import save_details, get_detail
10
  from prompts.prompts_manager import PromptsManager, Questions as Q
11
  from repository.repository import build_repo_from_environment, get_repository
12
  from repository import ModelRoles, Model
13
  from utils.parsing_utils import check_for_missing_answers
14
 
15
  user_msg = "Please describe what you need to do. To get the best results try to answer all the following questions:"
16
+
17
+ find_tags_regex = re.compile(r"(@\S)")
18
+
19
  class UIManager:
20
  def __init__(self):
21
  self.pm: PromptsManager = PromptsManager(work_categories=work_categories)
 
47
 
48
  def build_ui_for_parsing_answers(self):
49
  self._build_base_ui()
50
+ with st.status("parsing user input for tags"):
51
+ tags = find_tags_regex.findall(ss["user_input"])
52
+ details = [get_detail(t) for t in tags]
53
+ ss["details"] = details
54
  with st.status("initialising LLM"):
55
  self.repository.init()
56
  with st.status("waiting for LLM"):
57
+ answer = self.repository.send_prompt(self.pm.verify_user_input_prompt(ss["user_input"], details))
58
  st.write(f"answers from LLM: {answer['content']}")
59
  with st.status("Checking for missing answers"):
60
  answers = LlmParser.parse_verification_prompt_answers(answer['content'])
 
86
  with st.status("finding the work categories applicable to your work"):
87
  answer = self.repository.send_prompt(self.pm.get_work_category(ss["answers"][Q.WORK_TO_DO]))
88
  categories = LlmParser.parse_get_categories_answer(answer['content'])
89
+ ss["categories"] = categories
90
+ ss["step"] = "validate_data"
 
 
 
 
 
 
 
91
  st.rerun()
92
 
93
  def build_ui_for_form_created(self):
 
103
  del ss["signature"]
104
  st.rerun()
105
 
106
+ def _integrate_llm_answers_with_user_corrections(self):
107
+ for i in range(len(Q)):
108
+ ss["answers"][Q(i)] = ss[f"fq_{Q(i).name}"]
109
+
110
+ for details_key, func in [("your_details", self._get_personal_details),
111
+ ("location_details", self._get_location_details),
112
+ ("contractor_details", self._get_contractor_details)]:
113
+ details = func(details_key)
114
+ if details:
115
+ key = ss[details_key] # get the name under which this data should be saved
116
+ save_details(details, key)
117
+ ss["step"] = "check_categories"
118
+ st.rerun()
119
 
120
+ def _create_pdf_form(self):
121
+ with st.status("categories found, creating PDF form"):
122
+ form_data, filename = build_form_data_from_answers(ss["answers"], ss["categories"],
123
+ ss.get("signature"))
124
+ pdf_form = write_pdf_form(form_data)
125
+ pdf_form_filename = filename
126
+ ss["pdf_form"] = pdf_form
127
+ ss["pdf_form_filename"] = pdf_form_filename
128
+ ss["step"] = "form_created"
 
 
 
 
 
 
 
 
 
 
 
 
129
  st.rerun()
130
 
131
+ def build_ui_for_validate_data_after_correction(self):
132
+ self._build_validation_form(False, self._integrate_llm_answers_with_user_corrections)
133
+
134
+ def build_ui_to_confirm_form_data(self):
135
+ self._build_validation_form(True, self._create_pdf_form)
136
+
137
+
138
  @staticmethod
139
  def _get_personal_details(personal_details_key) -> PersonalDetails | None:
140
+ key_ = ss.get(personal_details_key)
141
+ if key_:
142
+ details = PersonalDetails(ss[f"fq_{Q.FULL_NAME.name}"], ss[f"fq_{Q.YOUR_EMAIL.name}"], ss[f"fq_{Q.CONTACT_NUMBER.name}"])
143
+ return details
144
  return None
145
 
146
  @staticmethod
 
158
  return None
159
 
160
 
161
+
162
+ def _build_validation_form(self, show_categories:bool, onsubmit):
163
+ def build_form_fragment(form_, col, title, add_save, *questions):
164
+ form_.text(title)
165
+ for user_data in questions:
166
+ with col:
167
+ form_.text_input(self.pm.questions_to_field_labels()[user_data], value=ss.get("answers", {})
168
+ .get(user_data), key=f"fq_{user_data.name}")
169
+ if add_save:
170
+ with col:
171
+ form_.text_input("Save as", key=title.replace(" ", "_"))
172
+
173
+ self._build_base_ui()
174
+ f = st.form("Please check the following information and correct fix any inaccuracies")
175
+ col1, col2 = f.columns(2)
176
+ build_form_fragment(f, col1, "your details", True, Q.FULL_NAME, Q.CONTACT_NUMBER, Q.YOUR_EMAIL)
177
+ build_form_fragment(f, col2, "work details", False, Q.WORK_TO_DO, Q.START_DATE, Q.END_DATE)
178
+ build_form_fragment(f, col1, "location details", True, Q.COMMUNITY, Q.BUILDING, Q.UNIT_APT_NUMBER,
179
+ Q.OWNER_OR_TENANT)
180
+ build_form_fragment(f, col2, "contractor details", True, Q.COMPANY_NAME, Q.COMPANY_NUMBER, Q.COMPANY_EMAIL)
181
+ if show_categories:
182
+ for k, wc in work_categories.items():
183
+ f.checkbox(label=wc, key=k, value=k in ss["categories"])
184
+
185
+ submit_data = f.form_submit_button()
186
+ if submit_data:
187
+ onsubmit()
188
+
189
  def use_streamlit():
190
 
191
  um = UIManager()
 
195
  if um.get_current_step() == "parsing_answers":
196
  um.build_ui_for_parsing_answers()
197
  if um.get_current_step() == "parsing_error":
198
+ um.build_ui_for_validate_data_after_correction()
199
  if um.get_current_step() == "ask_again":
200
  um.build_ui_for_ask_again()
201
  if um.get_current_step() == "check_category":
202
  um.build_ui_for_check_category()
203
+ if um.get_current_step() == "validate_form_data":
204
+ um.build_ui_to_confirm_form_data()
205
  if um.get_current_step() == "form_created":
206
  um.build_ui_for_form_created()
207
 
form/form.py CHANGED
@@ -116,4 +116,5 @@ work_categories = {"other_work": "other work", "civil": "Civil work, masonry",
116
  "kitchen_refurbish": "kitchen renovation or refurbishment",
117
  "lights_and_sockets": "installation of lights, ceiling lights, power sockets",
118
  "painting_wallpapering": "painting, wallpaper installation",
119
- "seating_area_barbecuing_fountain": "installation or renovation of outdoor structures such as seating area, barbecuing area or fountains"}
 
 
116
  "kitchen_refurbish": "kitchen renovation or refurbishment",
117
  "lights_and_sockets": "installation of lights, ceiling lights, power sockets",
118
  "painting_wallpapering": "painting, wallpaper installation",
119
+ "seating_area_barbecuing_fountain": "installation or renovation of outdoor structures such "
120
+ "as seating area, barbecuing area or fountains"}
local_storage/entities.py CHANGED
@@ -1,31 +1,37 @@
1
  import abc
2
- import json
3
 
4
- from streamlit_local_storage import LocalStorage
5
 
6
- ls: LocalStorage = LocalStorage()
 
 
 
 
 
 
 
7
 
8
 
9
  class SavedDetails(abc.ABC):
10
- def __init__(self, type_: str):
11
- self.type_ = type_
12
 
13
- def save_to_local_storage(self, key: str):
14
- ls.setItem(key, json.dumps(self.__dict__))
 
15
 
16
  @classmethod
17
- def load(cls, json_data: str):
18
- data = json.loads(json_data)
19
  type_ = data.get("type_")
20
  if not type_ or type_ != cls.type_:
21
  raise ValueError(f"the expected type is {cls.type_} but is actually {type_}")
22
  return cls.__init__(**{k: v for k, v in data if k != "type"})
23
 
 
 
24
 
25
  class PersonalDetails(SavedDetails):
26
- type_ = "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
@@ -33,9 +39,9 @@ class PersonalDetails(SavedDetails):
33
 
34
 
35
  class LocationDetails(SavedDetails):
36
- type_ = "location_details"
37
 
38
- def __init__(self, owner_or_tenant: str, community: str, building: str, unit_number: str):
39
  super().__init__(self.type_)
40
  self.owner_or_tenant = owner_or_tenant
41
  self.community = community
@@ -44,9 +50,9 @@ class LocationDetails(SavedDetails):
44
 
45
 
46
  class ContractorDetails(SavedDetails):
47
- type_ = "contractor_details"
48
 
49
- def __init__(self, contractor_name:str, contractor_contact_number:str, contractor_email:str):
50
  super().__init__(self.type_)
51
  self.contractor_name = contractor_name
52
  self.contractor_contact_number = contractor_contact_number
 
1
  import abc
2
+ from enum import Enum
3
 
 
4
 
5
+ class DetailsType(Enum):
6
+ PERSONAL_DETAILS = 1
7
+ LOCATION_DETAILS = 2
8
+ CONTRACTOR_DETAILS = 3
9
+
10
+ @classmethod
11
+ def values(cls):
12
+ return [cls.PERSONAL_DETAILS, cls.LOCATION_DETAILS, cls.CONTRACTOR_DETAILS]
13
 
14
 
15
  class SavedDetails(abc.ABC):
 
 
16
 
17
+ excluded_fields = ["type_"]
18
+ def __init__(self, type_: DetailsType):
19
+ self.type_ = type_
20
 
21
  @classmethod
22
+ def load(cls, data: dict):
 
23
  type_ = data.get("type_")
24
  if not type_ or type_ != cls.type_:
25
  raise ValueError(f"the expected type is {cls.type_} but is actually {type_}")
26
  return cls.__init__(**{k: v for k, v in data if k != "type"})
27
 
28
+ def to_json(self):
29
+ return {k:v for k,v in self.__dict__.items() if k not in self.excluded_fields}
30
 
31
  class PersonalDetails(SavedDetails):
32
+ type_ = DetailsType.PERSONAL_DETAILS
33
 
34
+ def __init__(self, full_name, email, contact_number, *_):
35
  super().__init__(self.type_)
36
  self.full_name = full_name
37
  self.email = email
 
39
 
40
 
41
  class LocationDetails(SavedDetails):
42
+ type_ = DetailsType.LOCATION_DETAILS
43
 
44
+ def __init__(self, owner_or_tenant: str, community: str, building: str, unit_number: str, *_):
45
  super().__init__(self.type_)
46
  self.owner_or_tenant = owner_or_tenant
47
  self.community = community
 
50
 
51
 
52
  class ContractorDetails(SavedDetails):
53
+ type_ = DetailsType.CONTRACTOR_DETAILS
54
 
55
+ def __init__(self, contractor_name: str, contractor_contact_number: str, contractor_email: str, *_):
56
  super().__init__(self.type_)
57
  self.contractor_name = contractor_name
58
  self.contractor_contact_number = contractor_contact_number
local_storage/ls_manager.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from streamlit_local_storage import LocalStorage
4
+
5
+ from local_storage.entities import DetailsType, SavedDetails, PersonalDetails, LocationDetails, ContractorDetails
6
+
7
+ ls = LocalStorage()
8
+
9
+
10
+ def get_detail(key: str):
11
+ for detail_type in DetailsType.values():
12
+ detail = json.loads((ls.getItem(detail_type.name) or {}).get(key))
13
+ if detail:
14
+ type_ = detail["type"]
15
+ if type_ == DetailsType.PERSONAL_DETAILS.name:
16
+ return PersonalDetails(**detail)
17
+ elif type_ == DetailsType.LOCATION_DETAILS.name:
18
+ return LocationDetails(**detail)
19
+ elif type_ == DetailsType.CONTRACTOR_DETAILS.name:
20
+ return ContractorDetails(**detail)
21
+ return None
22
+ return None
23
+
24
+
25
+ def save_details(details: SavedDetails, key: str):
26
+ if isinstance(details, PersonalDetails):
27
+ type_ = DetailsType.PERSONAL_DETAILS
28
+ elif isinstance(details, LocationDetails):
29
+ type_ = DetailsType.LOCATION_DETAILS
30
+ elif isinstance(details, ContractorDetails):
31
+ type_ = DetailsType.CONTRACTOR_DETAILS
32
+ else:
33
+ raise ValueError("Unexpected type: {}", type(details))
34
+ existing_data = json.loads(ls.getItem(type_.name) or "{}")
35
+ existing_data[key] = details.to_json()
36
+ ls.setItem(type_.name, existing_data, type_.name)
37
+
38
+
39
+ def get_details(type_: DetailsType):
40
+ return json.loads((ls.getItem(type_.name) or "{}"))
41
+
prompts/prompts_manager.py CHANGED
@@ -2,6 +2,7 @@ import datetime
2
  from enum import Enum
3
  from pathlib import Path
4
 
 
5
  from utils.date_utils import get_today_date_as_dd_mm_yyyy
6
 
7
 
@@ -33,13 +34,18 @@ class PromptsManager:
33
  todays_date = get_today_date_as_dd_mm_yyyy()
34
  verification_prompt = verification_prompt.replace("{today}", todays_date)
35
  self.verification_prompt: str = verification_prompt
 
36
 
37
- def verify_user_input_prompt(self, user_prompt) -> str:
38
- return (
39
- f"Using only this information \n {user_prompt} \n answer the following questions, for each question that you cannot answer just answer 'null'. "
40
- f"Put each answer in a new line, keep the answer brief "
41
- f"and maintain the order in which the questions are asked. Do not add any preamble: "
42
- f"{self.verification_prompt}")
 
 
 
 
43
 
44
  def get_work_category(self, work_description: str) -> str:
45
  return (
@@ -57,4 +63,11 @@ class PromptsManager:
57
  Questions.COMPANY_NAME: "Contractor company name", Questions.COMPANY_EMAIL: "Contracting company email",
58
  Questions.COMPANY_NUMBER: "Contracting company contact number", Questions.YOUR_EMAIL: "Your email"
59
  }
 
 
 
 
 
 
 
60
 
 
2
  from enum import Enum
3
  from pathlib import Path
4
 
5
+ from local_storage.entities import DetailsType, SavedDetails
6
  from utils.date_utils import get_today_date_as_dd_mm_yyyy
7
 
8
 
 
34
  todays_date = get_today_date_as_dd_mm_yyyy()
35
  verification_prompt = verification_prompt.replace("{today}", todays_date)
36
  self.verification_prompt: str = verification_prompt
37
+ self.verification_prompt_questions = self.verification_prompt.split("\n")
38
 
39
+ def verify_user_input_prompt(self, user_prompt, exclude_questions_group: list[SavedDetails] = None) -> str:
40
+ prompt = f"""
41
+ Using only this information \n {user_prompt} \n answer the following questions, for each question that you cannot answer just answer 'null'.
42
+ Put each answer in a new line, keep the answer brief
43
+ and maintain the order in which the questions are asked. Do not add any preamble:
44
+ """
45
+
46
+ skip_questions = [self.get_questions(d) for d in exclude_questions_group]
47
+ questions = [q for idx, q in enumerate(self.verification_prompt_questions) if idx not in skip_questions]
48
+ return prompt + "\n".join(questions)
49
 
50
  def get_work_category(self, work_description: str) -> str:
51
  return (
 
63
  Questions.COMPANY_NAME: "Contractor company name", Questions.COMPANY_EMAIL: "Contracting company email",
64
  Questions.COMPANY_NUMBER: "Contracting company contact number", Questions.YOUR_EMAIL: "Your email"
65
  }
66
+ def get_questions(self, type_:DetailsType):
67
+ if type_ == DetailsType.PERSONAL_DETAILS:
68
+ return [Questions.FULL_NAME, Questions.CONTACT_NUMBER, Questions.YOUR_EMAIL]
69
+ if type_ == DetailsType.CONTRACTOR_DETAILS:
70
+ return [Questions.COMPANY_NAME, Questions.COMPANY_NUMBER, Questions.COMPANY_EMAIL]
71
+ if type_ == DetailsType.LOCATION_DETAILS:
72
+ return [Questions.OWNER_OR_TENANT, Questions.COMMUNITY, Questions.BUILDING, Questions.UNIT_APT_NUMBER]
73