ofikodar commited on
Commit
ba6ea70
1 Parent(s): 78cc5bb

Upload 13 files

Browse files
app.py CHANGED
@@ -1,277 +1,20 @@
1
- import re
 
2
 
3
- import pdfkit
4
- import streamlit as st
5
 
6
- from src.chatbot.chatgpt import Chatgpt
7
- from src.utils import parse_pdf, build_html_resume
8
-
9
- section_examples = {'summary': 'I have passion for new tech',
10
- 'workExperience': 'Tell about my ability to lead projects',
11
- 'education': 'Describe my degree type in more details'}
12
-
13
- openai_key_info = 'https://platform.openai.com/account/api-keys'
14
-
15
-
16
- def list_section(section_name, section_data):
17
- description_key = 'description'
18
-
19
- item_keys = list(section_data[0].keys())
20
- item_keys.remove(description_key)
21
- for item_id, section_item in enumerate(section_data):
22
- cols = st.columns(len(item_keys))
23
- for col, key in zip(cols, item_keys):
24
- col.text_input(key, section_item[key], key=f'{section_name}_{item_id}_{key}')
25
- st.text_area(description_key, section_item[description_key], key=f'{section_name}_{item_id}_{description_key}')
26
-
27
- recruiter_subsection(section_name, section_example=section_examples[section_name], item_id=item_id)
28
- st.markdown('***')
29
-
30
-
31
- def skills_section(section_name, skills_data):
32
- num_columns = 3
33
- for skills_row in range(0, len(skills_data), num_columns):
34
- cols = st.columns([3, 1] * num_columns)
35
- skills_row_names = skills_data[skills_row: skills_row + num_columns]
36
- for item_id, skill in enumerate(skills_row_names):
37
- skill_id = skills_row + item_id
38
- cols[item_id * 2].text_input(' ', value=skill, key=f'{section_name}_{skill_id}', label_visibility='hidden')
39
- cols[item_id * 2 + 1].markdown('## ')
40
- if cols[item_id * 2 + 1].button('x', key=f'{section_name}_{skill_id}_remove_skill'):
41
- _remove_skill(skill_id, skills_data)
42
-
43
- skill_subsection(section_name)
44
- st.markdown('***')
45
-
46
-
47
- def _remove_skill(skill_id, skills_data):
48
- del skills_data[skill_id]
49
- st.experimental_rerun()
50
-
51
-
52
- def skill_subsection(section_name, item_id=0):
53
- key = f'{section_name}_{item_id}_add_skill'
54
- cols = st.columns([12, 1])
55
- new_skill = cols[0].text_input("Add skill", key=key)
56
- cols[1].markdown('##')
57
- clicked = cols[1].button("\+")
58
- if clicked and new_skill:
59
- st.write(new_skill)
60
- st.session_state['resume_data'][section_name].append(new_skill)
61
- st.write(st.session_state['resume_data'][section_name])
62
- st.experimental_rerun()
63
-
64
-
65
- def recruiter_subsection(section_name, section_example, item_id=0):
66
- with st.container():
67
- cols = st.columns([3, 10], gap='small')
68
- cols[0].write('\n')
69
- cols[0].write('\n')
70
- button_clicked = cols[0].button("Auto Section Improve", key=f'{section_name}_{item_id}_improve_auto')
71
- trigger_key = 'Add a special request'
72
- user_request_template = f"{trigger_key} to the bot here... e.g. {section_example}."
73
-
74
- user_request = cols[1].text_input("section_example", value=user_request_template,
75
- key=f'{section_name}_{item_id}_improve_manual', label_visibility='hidden')
76
- if button_clicked:
77
- user_request = '' if trigger_key in user_request else user_request
78
- section_key = get_item_key(section_name, item_id)
79
- section_text = st.session_state[section_key]
80
- new_section_text = st.session_state['chatbot'].improve_section(section_text, user_request)
81
-
82
- update_resume_data(new_section_text, section_name, item_id)
83
- st.experimental_rerun()
84
-
85
-
86
- def get_item_key(section_name, item_id=0):
87
- section_key = ''
88
- if section_name in ['workExperience', 'education']:
89
- key = 'description'
90
- section_key = f'{section_name}_{item_id}_{key}'
91
- elif section_name == 'summary':
92
- section_key = f'{section_name}'
93
- return section_key
94
-
95
-
96
- def update_resume_data(text_input, section_name, item_id=0):
97
- if section_name in ['workExperience', 'education']:
98
- key = 'description'
99
- st.session_state['resume_data'][section_name][item_id][key] = text_input
100
- elif section_name == 'summary':
101
- section_key = f'{section_name}'
102
- st.session_state['resume_data'][section_key] = text_input
103
-
104
-
105
- def summary_section(section_name, summary_data):
106
- st.text_area(section_name, summary_data, key=f'{section_name}', label_visibility='hidden')
107
- recruiter_subsection(section_name, section_examples[section_name])
108
-
109
-
110
- def contact_info_section(section_name, info_data):
111
- for key, value in info_data.items():
112
- if value:
113
- st.text_input(key.title(), value, key=f'{section_name}_{key}')
114
- st.markdown('***')
115
-
116
-
117
- def header():
118
- st.text_input('name', st.session_state.resume_data['name'], key="name")
119
- st.text_input('title', st.session_state.resume_data['title'], key="title")
120
-
121
-
122
- def body():
123
- section_dict = {'contactInfo': contact_info_section, 'summary': summary_section, 'workExperience': list_section,
124
- 'education': list_section, 'skills': skills_section}
125
- tabs_names = [key_to_tab_name(key) for key in section_dict.keys()]
126
- tabs = st.tabs(tabs_names)
127
- for tab, key in zip(tabs, section_dict):
128
- section_func = section_dict[key]
129
- with tab:
130
- section_func(key, st.session_state['resume_data'][key])
131
-
132
-
133
- def key_to_tab_name(input_string):
134
- return re.sub(r'([A-Z])', r' \1', input_string).strip().title()
135
-
136
-
137
- def sidebar():
138
- with st.sidebar:
139
- uploaded_file = st.file_uploader('Upload PDF Resume', type="PDF")
140
- if uploaded_file and _is_new_file(uploaded_file) and is_chatbot_loaded():
141
- _init_resume(uploaded_file)
142
-
143
- if is_data_loaded() and is_chatbot_loaded():
144
- st.button("Improve More", on_click=_improve_more)
145
- st.download_button('Download PDF', file_name='out.pdf', mime="application/json", data=download_pdf())
146
-
147
-
148
- def download_pdf():
149
- resume_data = format_resume_data()
150
- html_resume = build_html_resume(resume_data)
151
- options = {'page-size': 'A4', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in',
152
- 'margin-left': '0.75in', 'encoding': "UTF-8", 'no-outline': None}
153
- return pdfkit.from_string(html_resume, options=options, css='src/css/main.css')
154
-
155
-
156
- def _improve_more():
157
- print("Improving resume")
158
- st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(st.session_state['resume_data'])
159
-
160
-
161
- def _init_chatbot():
162
- cols = st.columns([6, 1, 1])
163
- api_key = cols[0].text_input("Enter OpenAI API key")
164
- cols[1].markdown("#")
165
- api_submit = cols[1].button("Submit")
166
-
167
- cols[2].markdown("#")
168
- get_info = cols[2].button("Get key")
169
- if get_info:
170
- st.info(f"Get your key at: {openai_key_info}")
171
- if api_submit:
172
- if Chatgpt.validate_api(api_key):
173
- st.session_state['chatbot'] = Chatgpt(api_key)
174
- st.experimental_rerun()
175
-
176
- else:
177
- st.error("Not valid API key - try again...")
178
-
179
-
180
- def is_chatbot_loaded():
181
- return st.session_state.get('chatbot')
182
-
183
-
184
- def _is_new_file(uploaded_file):
185
- return uploaded_file.id != st.session_state.get('file_id', '')
186
-
187
-
188
- def _init_resume(uploaded_file):
189
- resume_data = parse_pdf(uploaded_file)
190
- st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(resume_data)
191
- st.session_state['file_id'] = uploaded_file.id
192
- st.experimental_rerun()
193
-
194
-
195
- def format_resume_data():
196
- current_state = st.session_state
197
- resume_data = {}
198
- contact_info = {}
199
- work_experience = []
200
- education = []
201
- skills = []
202
-
203
- resume_data['name'] = current_state.get('name', '')
204
- resume_data['title'] = current_state.get('title', '')
205
-
206
- contact_info_keys = ['linkedin', 'github', 'email', 'address']
207
- for key in contact_info_keys:
208
- contact_info[key] = current_state.get(f'contactInfo_{key}', '')
209
- resume_data['contactInfo'] = contact_info
210
-
211
- resume_data['summary'] = current_state.get('summary', '')
212
-
213
- work_experience_keys = ['workExperience_{}_title', 'workExperience_{}_company', 'workExperience_{}_dates',
214
- 'workExperience_{}_description']
215
- education_keys = ['education_{}_degree', 'education_{}_school', 'education_{}_dates', 'education_{}_description']
216
-
217
- total_work_experience = count_entries(st.session_state, 'workExperience')
218
- total_education = count_entries(st.session_state, 'education')
219
-
220
- for i in range(total_work_experience):
221
- work_experience.append(
222
- {key.split('_')[2]: current_state.get(key.format(i), '') for key in work_experience_keys})
223
-
224
- for i in range(total_education):
225
- education.append({key.split('_')[2]: current_state.get(key.format(i), '') for key in education_keys})
226
-
227
- resume_data['workExperience'] = work_experience
228
- resume_data['education'] = education
229
-
230
- total_skills = count_entries(st.session_state, 'skills')
231
-
232
- for i in range(total_skills):
233
- skill_key = f'skills_{i}'
234
-
235
- skills.append(current_state.get(skill_key, ''))
236
- resume_data['skills'] = skills
237
-
238
- return resume_data
239
-
240
-
241
- def count_entries(input_dict, entry_type):
242
- max_index = max([int(key.split("_")[1]) for key in input_dict.keys() if key.startswith(f"{entry_type}_")],
243
- default=0)
244
- return max_index + 1
245
-
246
-
247
- def title():
248
- st.title("ChatCV - AI Resume Builder")
249
-
250
-
251
- def upload_resume_header():
252
- st.success("Upload PDF Resume - Let the magic begin...")
253
-
254
-
255
- def is_data_loaded():
256
- return st.session_state.get('resume_data')
257
-
258
-
259
- def _main():
260
  title()
261
  if is_chatbot_loaded():
262
  sidebar()
263
 
264
  if is_data_loaded():
265
- header()
266
  body()
267
-
268
  else:
269
- upload_resume_header()
270
  else:
271
- _init_chatbot()
272
 
273
 
274
  if __name__ == '__main__':
275
- _main()
276
-
277
- # bootstrap 4 collapse example
 
1
+ from src.ui import *
2
+ from src.utils import is_chatbot_loaded
3
 
 
 
4
 
5
+ def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  title()
7
  if is_chatbot_loaded():
8
  sidebar()
9
 
10
  if is_data_loaded():
11
+ resume_header()
12
  body()
 
13
  else:
14
+ user_info()
15
  else:
16
+ init_chatbot()
17
 
18
 
19
  if __name__ == '__main__':
20
+ main()
 
 
src/chatbot/chatgpt.py CHANGED
@@ -1,18 +1,20 @@
1
  import ast
2
  import json
 
3
  import re
4
  from pathlib import Path
5
  from typing import Dict
6
- import logging
7
 
8
  import requests
9
  from revChatGPT.Official import Chatbot
10
 
11
- from .prompts import get_prompt, data_format
12
 
13
  logging.basicConfig(filename='chatgpt.log', level=logging.INFO, format='%(asctime)s %(message)s',
14
  datefmt='%m/%d/%Y %I:%M:%S %p')
15
 
 
 
16
 
17
  class Chatgpt:
18
  def __init__(self, api_key):
@@ -61,11 +63,13 @@ class Chatgpt:
61
 
62
  def _ask(self, chatgpt_input):
63
  logging.info("Asking chatbot for response")
64
- response = self.chatbot.ask(chatgpt_input)
65
- answer = response['choices'][0]['text']
66
- logging.info("Received response from chatbot")
67
- logging.info(f"Response: {answer}")
68
-
 
 
69
  return answer
70
 
71
  def parse_json_from_string(self, json_string):
@@ -113,9 +117,8 @@ class Chatgpt:
113
 
114
  def clean_section_response(self, input_string):
115
  try:
116
- start = input_string.index('"')
117
- end = input_string.rindex('"') + 1
118
- input_string = input_string[start:end]
119
  except ValueError:
120
  pass
121
  input_string = self.remove_prefix(input_string)
 
1
  import ast
2
  import json
3
+ import logging
4
  import re
5
  from pathlib import Path
6
  from typing import Dict
 
7
 
8
  import requests
9
  from revChatGPT.Official import Chatbot
10
 
11
+ from src.chatbot.prompts import get_prompt, data_format
12
 
13
  logging.basicConfig(filename='chatgpt.log', level=logging.INFO, format='%(asctime)s %(message)s',
14
  datefmt='%m/%d/%Y %I:%M:%S %p')
15
 
16
+ openai_key_info = 'https://platform.openai.com/account/api-keys'
17
+
18
 
19
  class Chatgpt:
20
  def __init__(self, api_key):
 
63
 
64
  def _ask(self, chatgpt_input):
65
  logging.info("Asking chatbot for response")
66
+ try:
67
+ response = self.chatbot.ask(chatgpt_input)
68
+ answer = response['choices'][0]['text']
69
+ logging.info("Received response from chatbot")
70
+ logging.info(f"Response: {answer}")
71
+ except Exception:
72
+ answer = ""
73
  return answer
74
 
75
  def parse_json_from_string(self, json_string):
 
117
 
118
  def clean_section_response(self, input_string):
119
  try:
120
+ input_string = re.sub('^\W*"', "", input_string)
121
+ input_string = re.sub('"\W*$', "", input_string)
 
122
  except ValueError:
123
  pass
124
  input_string = self.remove_prefix(input_string)
src/chatbot/prompts.py CHANGED
@@ -6,8 +6,10 @@ data_format = {'name': '', 'title': '',
6
  {'title': '', 'company': '', 'dates': '', 'description': ''}, ],
7
  'education': [{'degree': '', 'school': '', 'dates': '', 'description': ''}, ], 'skills': ['', '', '']}
8
 
9
- recruiter_prompt = 'You are a recruiter and a professional resume builder.'
10
- command_prompt = 'Re-write the input as professionally as possible, adding vital, valuable information and skills.'
 
 
11
  user_request_prompt = f'{prompt_placeholder}'
12
 
13
  output_commands_prompts = dict()
@@ -29,4 +31,4 @@ def get_prompt(input_data, user_request='', output_type='all'):
29
  template = '\n'.join(
30
  [recruiter_prompt, command_prompt, user_request_prompt.replace(prompt_placeholder, user_request),
31
  input_prompt.replace(prompt_placeholder, input_data), output_commands_prompts[output_type], command_prompt])
32
- return template
 
6
  {'title': '', 'company': '', 'dates': '', 'description': ''}, ],
7
  'education': [{'degree': '', 'school': '', 'dates': '', 'description': ''}, ], 'skills': ['', '', '']}
8
 
9
+ recruiter_prompt = 'You are a professional resume builder and a recruiter.\n'
10
+ command_prompt = 'Re-write the input as professionally as possible, adding vital, valuable information and skills.\n' \
11
+ 'Enhance the input to showcase the relevant education, experience, and skills in a professional manner to effectively demonstrate value to potential employers.\n' \
12
+ f'Do it for every value in your output {str(list(data_format))}. '
13
  user_request_prompt = f'{prompt_placeholder}'
14
 
15
  output_commands_prompts = dict()
 
31
  template = '\n'.join(
32
  [recruiter_prompt, command_prompt, user_request_prompt.replace(prompt_placeholder, user_request),
33
  input_prompt.replace(prompt_placeholder, input_data), output_commands_prompts[output_type], command_prompt])
34
+ return template
src/css/main.css CHANGED
@@ -24,6 +24,8 @@ html
24
 
25
  body
26
  {
 
 
27
  top: 0px;
28
  -webkit-font-smoothing:antialiased;
29
  font-family:Lora, serif;
@@ -44,7 +46,7 @@ body
44
 
45
  h1
46
  {
47
- color:rgba(0,0,0,1);
48
  }
49
 
50
  .wrapper
@@ -57,6 +59,7 @@ h1
57
 
58
  .left
59
  {
 
60
 
61
  left: 0px;
62
  height:100%;
@@ -68,11 +71,13 @@ left: 0px;
68
 
69
  .right
70
  {
 
 
71
  right: 0px;
72
  position: absolute;
73
  text-align:center;
74
- background-color:rgba(200,0,0,.025);
75
- border-left:1px solid rgba(230,0,0,.05);
76
 
77
  height:100%;
78
  width:30%;
@@ -81,11 +86,35 @@ position: absolute;
81
 
82
  }
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
 
86
  .name-hero
87
  {
88
  background:rgba(0,0,0,.001);
 
89
 
90
  width:85%;
91
  }
@@ -108,9 +137,34 @@ position: absolute;
108
  .name-hero h3
109
  {
110
  font-family:Open Sans, sans-serif;
 
111
  font-size:1.5em;
112
  text-align:center;
113
  margin:0px auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
  .name-hero h1 em
@@ -122,6 +176,7 @@ position: absolute;
122
 
123
  .name-hero p
124
  {
 
125
  color:rgba(0,0,0,1);
126
  font-size:.75em;
127
  line-height:1.5;
@@ -133,10 +188,13 @@ position: absolute;
133
  {
134
  font-size:1.5em;
135
  margin:0 auto;
 
136
 
137
  width:85%;
138
  }
139
 
 
 
140
  .inner
141
  {
142
  margin:0 auto;
@@ -156,7 +214,7 @@ position: absolute;
156
 
157
  .inner p em
158
  {
159
- color:rgba(0,0,0,1);
160
  font-style:normal;
161
  }
162
 
@@ -175,6 +233,7 @@ ul
175
 
176
  .skill-set
177
  {
 
178
 
179
  color:rgba(0,0,0,1);
180
  list-style:none;
 
24
 
25
  body
26
  {
27
+ font-family:Open Sans, sans-serif;
28
+
29
  top: 0px;
30
  -webkit-font-smoothing:antialiased;
31
  font-family:Lora, serif;
 
46
 
47
  h1
48
  {
49
+ color: #145DA0;
50
  }
51
 
52
  .wrapper
 
59
 
60
  .left
61
  {
62
+ font-family:Open Sans, sans-serif;
63
 
64
  left: 0px;
65
  height:100%;
 
71
 
72
  .right
73
  {
74
+ font-family:Open Sans, sans-serif;
75
+
76
  right: 0px;
77
  position: absolute;
78
  text-align:center;
79
+ background-color:rgba(0, 0, 0,0.1);
80
+ border-left:1px solid rgba(0,0,0,.1);
81
 
82
  height:100%;
83
  width:30%;
 
86
 
87
  }
88
 
89
+ .right h1
90
+ {
91
+
92
+ color: #145DA0;
93
+ }
94
+
95
+ .right h2
96
+ {
97
+
98
+ color: #145DA0;
99
+ }
100
+
101
+
102
+
103
+ .right p
104
+ {
105
+ color: #145DA0;
106
+ }
107
+
108
+ .left p
109
+ {
110
+ color: #145DA0;
111
+ }
112
 
113
 
114
  .name-hero
115
  {
116
  background:rgba(0,0,0,.001);
117
+ margin:0px 1px 0 0;
118
 
119
  width:85%;
120
  }
 
137
  .name-hero h3
138
  {
139
  font-family:Open Sans, sans-serif;
140
+
141
  font-size:1.5em;
142
  text-align:center;
143
  margin:0px auto;
144
+ padding-top: -50 px;
145
+ }
146
+
147
+
148
+ .title h3
149
+ {
150
+ font-family:Open Sans, sans-serif;
151
+
152
+ font-size:1.5em;
153
+ text-align:center;
154
+ margin:0px auto;
155
+ color:rgba(128, 128, 128,0.5);
156
+ padding-top: -50 px;
157
+ }
158
+
159
+ .name-hero title
160
+ {
161
+ font-family:Open Sans, sans-serif;
162
+
163
+ font-size:1.5em;
164
+ text-align:center;
165
+ margin:0px auto;
166
+ color:rgba(128, 128, 128,0.5);
167
+ padding-top: -0 px;
168
  }
169
 
170
  .name-hero h1 em
 
176
 
177
  .name-hero p
178
  {
179
+ font-family:Open Sans, sans-serif;
180
  color:rgba(0,0,0,1);
181
  font-size:.75em;
182
  line-height:1.5;
 
188
  {
189
  font-size:1.5em;
190
  margin:0 auto;
191
+ color: #145DA0;
192
 
193
  width:85%;
194
  }
195
 
196
+
197
+
198
  .inner
199
  {
200
  margin:0 auto;
 
214
 
215
  .inner p em
216
  {
217
+ color: #145DA0;
218
  font-style:normal;
219
  }
220
 
 
233
 
234
  .skill-set
235
  {
236
+ font-family:Open Sans, sans-serif;
237
 
238
  color:rgba(0,0,0,1);
239
  list-style:none;
src/data_handler.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pdfkit
2
+ import streamlit as st
3
+
4
+ from src.exceptions import PDFSizeException
5
+ from src.pdf_handler import build_html_resume, parse_pdf
6
+ from src.utils import count_entries
7
+
8
+
9
+ def init_resume(uploaded_file):
10
+ resume_data, num_pages = parse_pdf(uploaded_file)
11
+ if num_pages > 3:
12
+ raise PDFSizeException
13
+ st.session_state['file_id'] = uploaded_file.id
14
+ return resume_data
15
+
16
+
17
+ def update_resume_data(text_input, section_name, item_id=0):
18
+ if section_name in ['workExperience', 'education']:
19
+ key = 'description'
20
+ st.session_state['resume_data'][section_name][item_id][key] = text_input
21
+ elif section_name == 'summary':
22
+ section_key = f'{section_name}'
23
+ st.session_state['resume_data'][section_key] = text_input
24
+
25
+
26
+ @st.cache
27
+ def download_pdf():
28
+ if st.session_state.get('name'):
29
+ resume_data = format_resume_data()
30
+ else:
31
+ resume_data = st.session_state['resume_data']
32
+ html_resume = build_html_resume(resume_data)
33
+ options = {'page-size': 'A4', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in',
34
+ 'margin-left': '0.75in', 'encoding': "UTF-8", 'no-outline': None}
35
+ return pdfkit.from_string(html_resume, options=options, css='src/css/main.css')
36
+
37
+
38
+ def improve_resume(resume_data=None):
39
+ if resume_data is not None:
40
+ st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(resume_data)
41
+ else:
42
+ st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(st.session_state['resume_data'])
43
+
44
+
45
+ def format_resume_data():
46
+ current_state = st.session_state
47
+ resume_data = {}
48
+ contact_info = {}
49
+ work_experience = []
50
+ education = []
51
+ skills = []
52
+
53
+ resume_data['name'] = current_state.get('name', '')
54
+ resume_data['title'] = current_state.get('title', '')
55
+
56
+ contact_info_keys = ['linkedin', 'github', 'email', 'address']
57
+ for key in contact_info_keys:
58
+ contact_info[key] = current_state.get(f'contactInfo_{key}', '')
59
+
60
+ resume_data['contactInfo'] = contact_info
61
+
62
+ resume_data['summary'] = current_state.get('summary', '')
63
+
64
+ work_experience_keys = ['workExperience_{}_title', 'workExperience_{}_company', 'workExperience_{}_dates',
65
+ 'workExperience_{}_description']
66
+ education_keys = ['education_{}_degree', 'education_{}_school', 'education_{}_dates', 'education_{}_description']
67
+
68
+ total_work_experience = count_entries(st.session_state, 'workExperience')
69
+ total_education = count_entries(st.session_state, 'education')
70
+
71
+ for i in range(total_work_experience):
72
+ work_experience.append(
73
+ {key.split('_')[2]: current_state.get(key.format(i), '') for key in work_experience_keys})
74
+
75
+ for i in range(total_education):
76
+ education.append({key.split('_')[2]: current_state.get(key.format(i), '') for key in education_keys})
77
+
78
+ resume_data['workExperience'] = work_experience
79
+ resume_data['education'] = education
80
+
81
+ total_skills = count_entries(st.session_state, 'skills')
82
+
83
+ for i in range(total_skills):
84
+ skill_key = f'skills_{i}'
85
+
86
+ skills.append(current_state.get(skill_key, ''))
87
+ resume_data['skills'] = skills
88
+
89
+ return resume_data
src/exceptions.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class PDFSizeException(Exception):
2
+ """Raised when the input value is less than 3"""
3
+ pass
4
+
5
+
6
+ class ChatbotInitException(Exception):
7
+ """Raised when there's a problem with chabot init"""
8
+ pass
9
+
10
+
11
+ class ChatbotAPIException(Exception):
12
+ """Raised when there's a problem with openai api"""
13
+ pass
src/pdf_handler.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import PyPDF2
2
+ from jinja2 import FileSystemLoader, Environment
3
+
4
+
5
+ def parse_pdf(pdf_file):
6
+ if pdf_file is isinstance(pdf_file, str):
7
+ with open(pdf_file, "rb") as file:
8
+ return _parse(file)
9
+ else:
10
+ return _parse(pdf_file)
11
+
12
+
13
+ def _parse(file):
14
+ reader = PyPDF2.PdfReader(file)
15
+ pdf_text = []
16
+ num_pages = len(reader.pages)
17
+ # Iterate over each page
18
+ for page_number in range(num_pages):
19
+ # Get the current page
20
+ page = reader.pages[page_number]
21
+
22
+ # Extract the text from the page
23
+ page_text = page.extract_text()
24
+
25
+ pdf_text.append(page_text)
26
+ pdf_text = '\n'.join(pdf_text)
27
+ return pdf_text, num_pages
28
+
29
+
30
+ def build_html_resume(data):
31
+ env = Environment(loader=FileSystemLoader('src/templates'))
32
+ template = env.get_template('resume.html')
33
+ html_resume = template.render(data)
34
+ return html_resume
35
+
36
+
37
+ def export_html(html_resume, output_path):
38
+ with open(output_path, 'w', encoding='utf8') as f:
39
+ f.write(html_resume)
src/templates/resume.html CHANGED
@@ -6,10 +6,13 @@
6
  <div class="inner">
7
  <div class="name-hero">
8
  <div class="name-text">
9
- <h1>{{name}} </h1>
10
- <h3>{{title}} </h3>
11
  </div>
12
 
 
 
 
13
  </div>
14
 
15
 
@@ -37,7 +40,7 @@
37
  </div>
38
  </div>
39
  <div class="right">
40
- <h1>Contact Information</h1>
41
  <p>Email: {{email}}</p>
42
  <p>Phone: {{phone}}</p>
43
  <p>Address: {{address}}</p>
 
6
  <div class="inner">
7
  <div class="name-hero">
8
  <div class="name-text">
9
+ <h3>{{name}} </h3>
10
+
11
  </div>
12
 
13
+ <div class="title">
14
+ <h3> {{title}} </h3>
15
+ </div>
16
  </div>
17
 
18
 
 
40
  </div>
41
  </div>
42
  <div class="right">
43
+ <h2>Contact Information</h2>
44
  <p>Email: {{email}}</p>
45
  <p>Phone: {{phone}}</p>
46
  <p>Address: {{address}}</p>
src/ui.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ from src.chatbot.chatgpt import openai_key_info, Chatgpt
4
+ from src.data_handler import improve_resume, init_resume, download_pdf, update_resume_data, PDFSizeException
5
+ from src.exceptions import ChatbotInitException
6
+ from src.utils import is_new_file, is_data_loaded, key_to_tab_name, get_item_key, init_user_info
7
+
8
+ section_examples = {'summary': 'I have passion for new tech',
9
+ 'workExperience': 'Tell about my ability to lead projects',
10
+ 'education': 'Describe my degree type in more details'}
11
+
12
+
13
+ def title():
14
+ st.title("ChatCV - AI Resume Builder")
15
+
16
+
17
+ def resume_header():
18
+ st.text_input('name', st.session_state.resume_data['name'], key="name")
19
+ st.text_input('title', st.session_state.resume_data['title'], key="title")
20
+
21
+
22
+ def unknown_error():
23
+ st.session_state['user_info'] = init_user_info(error_info, "It's just a glitch in the matrix."
24
+ " Try hitting refresh, and if that doesn't work, just imagine yourself in a peaceful place.")
25
+ user_info()
26
+
27
+
28
+ def user_info():
29
+ if not st.session_state.get('user_info'):
30
+ upload_resume_header()
31
+
32
+ message_type = st.session_state['user_info']['message_type']
33
+ message = st.session_state['user_info']['message']
34
+ message_type(message)
35
+
36
+
37
+ def upload_resume_header():
38
+ st.session_state['user_info'] = init_user_info(st.success, "Upload PDF Resume - Let the magic begin. \n\n"
39
+ "This may take a bit... Grub a warm cup of coffee while we working :)")
40
+
41
+
42
+ def upload(uploaded_file):
43
+ try:
44
+ resume_data = init_resume(uploaded_file)
45
+ st.session_state['user_info'] = init_user_info(success_info, "Working on it...")
46
+ improve_resume(resume_data)
47
+
48
+ except PDFSizeException:
49
+ st.session_state['user_info'] = init_user_info(error_info, "PDF size max size is 4, try upload again...")
50
+
51
+ except Exception:
52
+ st.session_state['user_info'] = init_user_info(error_info, "PDF upload, try upload again...")
53
+
54
+
55
+ def sidebar():
56
+ with st.sidebar:
57
+ uploaded_file = st.file_uploader('Upload PDF Resume', type=["PDF"])
58
+ if uploaded_file and is_new_file(uploaded_file):
59
+ upload(uploaded_file)
60
+ st.experimental_rerun()
61
+
62
+ if is_data_loaded():
63
+ st.button("Improve More", on_click=improve_resume)
64
+ st.download_button('Download PDF', file_name='out.pdf', mime="application/json", data=download_pdf())
65
+
66
+
67
+ def body():
68
+ section_dict = {'contactInfo': contact_info_section, 'summary': summary_section, 'workExperience': list_section,
69
+ 'education': list_section, 'skills': skills_section}
70
+ tabs_names = [key_to_tab_name(key) for key in section_dict.keys()]
71
+ tabs = st.tabs(tabs_names)
72
+ for tab, key in zip(tabs, section_dict):
73
+ section_func = section_dict[key]
74
+ with tab:
75
+ section_func(key, st.session_state['resume_data'][key])
76
+
77
+
78
+ def init_chatbot():
79
+ cols = st.columns([6, 1, 1])
80
+ api_key = cols[0].text_input("Enter OpenAI API key")
81
+ cols[1].markdown("#")
82
+ api_submit = cols[1].button("Submit")
83
+
84
+ cols[2].markdown("#")
85
+ get_info = cols[2].button("Get key")
86
+ if get_info:
87
+ st.info(f"Get your key at: {openai_key_info}")
88
+ if api_submit:
89
+ if Chatgpt.validate_api(api_key):
90
+ try:
91
+ st.session_state['chatbot'] = Chatgpt(api_key)
92
+ except ChatbotInitException:
93
+ st.session_state['user_info'] = init_user_info(error_info,
94
+ "Error with Chatbot loadin, please refresh...")
95
+
96
+ st.experimental_rerun()
97
+
98
+ else:
99
+ st.error("Not valid API key - try again...")
100
+
101
+
102
+ def summary_section(section_name, summary_data):
103
+ st.text_area(section_name, summary_data, key=f'{section_name}', label_visibility='hidden')
104
+ recruiter_subsection(section_name, section_examples[section_name])
105
+
106
+
107
+ def list_section(section_name, section_data):
108
+ description_key = 'description'
109
+
110
+ item_keys = list(section_data[0].keys())
111
+ item_keys.remove(description_key)
112
+ for item_id, section_item in enumerate(section_data):
113
+ cols = st.columns(len(item_keys))
114
+ for col, key in zip(cols, item_keys):
115
+ col.text_input(key, section_item[key], key=f'{section_name}_{item_id}_{key}')
116
+ st.text_area(description_key, section_item[description_key], key=f'{section_name}_{item_id}_{description_key}')
117
+
118
+ recruiter_subsection(section_name, section_example=section_examples[section_name], item_id=item_id)
119
+ st.markdown('***')
120
+
121
+
122
+ def skills_section(section_name, skills_data):
123
+ num_columns = 3
124
+ for skills_row in range(0, len(skills_data), num_columns):
125
+ cols = st.columns([3, 1] * num_columns)
126
+ skills_row_names = skills_data[skills_row: skills_row + num_columns]
127
+ for item_id, skill in enumerate(skills_row_names):
128
+ skill_id = skills_row + item_id
129
+ cols[item_id * 2].text_input(' ', value=skill, key=f'{section_name}_{skill_id}', label_visibility='hidden')
130
+ cols[item_id * 2 + 1].markdown('## ')
131
+ if cols[item_id * 2 + 1].button('x', key=f'{section_name}_{skill_id}remove_from_list'):
132
+ del skills_data[skill_id]
133
+ st.experimental_rerun()
134
+
135
+ skill_subsection(section_name)
136
+ st.markdown('***')
137
+
138
+
139
+ def contact_info_section(section_name, info_data):
140
+ for key, value in info_data.items():
141
+ if value:
142
+ st.text_input(key.title(), value, key=f'{section_name}_{key}')
143
+ st.markdown('***')
144
+
145
+
146
+ def skill_subsection(section_name, item_id=0):
147
+ key = f'{section_name}_{item_id}_add_skill'
148
+ cols = st.columns([12, 1])
149
+ new_skill = cols[0].text_input("Add skill", key=key)
150
+ cols[1].markdown('##')
151
+ clicked = cols[1].button("\+")
152
+ if clicked and new_skill:
153
+ st.write(new_skill)
154
+ st.session_state['resume_data'][section_name].append(new_skill)
155
+ st.write(st.session_state['resume_data'][section_name])
156
+ st.experimental_rerun()
157
+
158
+
159
+ def recruiter_subsection(section_name, section_example, item_id=0):
160
+ with st.container():
161
+ cols = st.columns([3, 10], gap='small')
162
+ cols[0].write('\n')
163
+ cols[0].write('\n')
164
+ button_clicked = cols[0].button("Auto Section Improve", key=f'{section_name}_{item_id}_improve_auto')
165
+ trigger_key = 'Add a special request'
166
+ user_request_template = f"{trigger_key} to the bot here... e.g. {section_example}."
167
+
168
+ user_request = cols[1].text_input("section_example", value=user_request_template,
169
+ key=f'{section_name}_{item_id}_improve_manual', label_visibility='hidden')
170
+ if button_clicked:
171
+ user_request = '' if trigger_key in user_request else user_request
172
+ section_key = get_item_key(section_name, item_id)
173
+ section_text = st.session_state[section_key]
174
+ new_section_text = st.session_state['chatbot'].improve_section(section_text, user_request)
175
+
176
+ update_resume_data(new_section_text, section_name, item_id)
177
+ st.experimental_rerun()
178
+
179
+
180
+ def success_info(message):
181
+ st.success(message)
182
+
183
+
184
+ def error_info(message):
185
+ st.error(message)
src/utils.py CHANGED
@@ -1,38 +1,42 @@
1
- import PyPDF2
2
- from jinja2 import FileSystemLoader, Environment
3
 
 
4
 
5
- def parse_pdf(pdf_file):
6
- if pdf_file is isinstance(pdf_file, str):
7
- with open(pdf_file, "rb") as file:
8
- return _parse(file)
9
- else:
10
- return _parse(pdf_file)
11
 
 
 
12
 
13
- def _parse(file):
14
- reader = PyPDF2.PdfReader(file)
15
- pdf_text = []
16
- # Iterate over each page
17
- for page_number in range(len(reader.pages)):
18
- # Get the current page
19
- page = reader.pages[page_number]
20
 
21
- # Extract the text from the page
22
- page_text = page.extract_text()
23
 
24
- pdf_text.append(page_text)
25
- pdf_text = '\n'.join(pdf_text)
26
- return pdf_text
27
 
 
 
28
 
29
- def build_html_resume(data):
30
- env = Environment(loader=FileSystemLoader('src/templates'))
31
- template = env.get_template('resume.html')
32
- html_resume = template.render(data)
33
- return html_resume
34
 
 
 
35
 
36
- def export_html(html_resume, output_path):
37
- with open(output_path, 'w', encoding='utf8') as f:
38
- f.write(html_resume)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
 
2
 
3
+ import streamlit as st
4
 
 
 
 
 
 
 
5
 
6
+ def is_chatbot_loaded():
7
+ return st.session_state.get('chatbot')
8
 
 
 
 
 
 
 
 
9
 
10
+ def is_new_file(uploaded_file):
11
+ return uploaded_file.id != st.session_state.get('file_id', '')
12
 
 
 
 
13
 
14
+ def is_data_loaded():
15
+ return st.session_state.get('resume_data')
16
 
 
 
 
 
 
17
 
18
+ def key_to_tab_name(input_string):
19
+ return re.sub(r'([A-Z])', r' \1', input_string).strip().title()
20
 
21
+
22
+ def count_entries(input_dict, entry_type):
23
+ max_index = max([int(key.split("_")[1]) for key in input_dict.keys() if key.startswith(f"{entry_type}_")],
24
+ default=0)
25
+ return max_index + 1
26
+
27
+
28
+ def get_item_key(section_name, item_id=0):
29
+ section_key = ''
30
+ if section_name in ['workExperience', 'education']:
31
+ key = 'description'
32
+ section_key = f'{section_name}_{item_id}_{key}'
33
+ elif section_name == 'summary':
34
+ section_key = f'{section_name}'
35
+ return section_key
36
+
37
+
38
+ def init_user_info(message_type, message):
39
+ return {
40
+ 'message_type': message_type,
41
+ 'message': message
42
+ }