Sudhir878786 commited on
Commit
d8d4fef
1 Parent(s): fb8b705

error fixed

Browse files
.gitattributes CHANGED
@@ -25,7 +25,6 @@
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
  *.wasm filter=lfs diff=lfs merge=lfs -text
 
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
28
  *.tflite filter=lfs diff=lfs merge=lfs -text
29
  *.tgz filter=lfs diff=lfs merge=lfs -text
30
  *.wasm filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.8 image as the base image
2
+ FROM python:3.8-buster
3
+
4
+ # Install the necessary dependencies
5
+ RUN apt-get update && apt-get install -y wget
6
+
7
+ # Download the wkhtmltopdf package
8
+ RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb
9
+
10
+ # Install the package
11
+ RUN apt-get install -y --no-install-recommends ./wkhtmltox_0.12.6-1.buster_amd64.deb
12
+
13
+ # Copy the requirements.txt file
14
+ COPY requirements.txt /app/
15
+
16
+ # Change the working directory
17
+ WORKDIR /app/
18
+
19
+ # Install the Python dependencies
20
+ RUN pip install -r requirements.txt
21
+
22
+ # Copy the rest of the app files
23
+ COPY src/ /app/src/
24
+ COPY app.py /app/
25
+
26
+
27
+ # Expose port 7860
28
+ EXPOSE 7860
29
+
30
+ # Set the command to run when the container starts
31
+ CMD ["python3", "-m" ,"streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.enableXsrfProtection=false"]
32
+
README.md CHANGED
@@ -1,13 +1,22 @@
1
  ---
2
- title: ResumeGPT
3
  emoji: 📊
4
- colorFrom: purple
5
- colorTo: purple
6
- sdk: streamlit
7
- sdk_version: 1.21.0
8
- app_file: app.py
9
  pinned: false
10
- license: openrail
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ResumeGPT Resume Builder
3
  emoji: 📊
4
+ colorFrom: indigo
5
+ colorTo: indigo
6
+ sdk: docker
 
 
7
  pinned: false
 
8
  ---
9
 
10
+ <html>
11
+ <head>
12
+ <title>ResumeGPT - AI Resume Builder</title>
13
+ </head>
14
+ <body>
15
+ <h1>ResumeGPT - AI Resume Builder</h1>
16
+ <p>ResumeGPT is a streamlit web app that uses the OpenAI ChatGPT model to improve your resume. It takes in your existing resume in pdf format, processes it, and then sends it to a chatbot that acts as a recruiter. The chatbot will then provide suggestions for improvements to your resume, which you can then edit and save as a pdf file.</p>
17
+ <p>Please note that the processing time may take a few minutes, and it is possible for the chatbot to return some inaccuracies. It is recommended to double check the information before finalizing your improved resume.</p>
18
+ <p>To use ResumeGPT, you will need to have an OpenAI API Key, which can be obtained from <a href="https://platform.openai.com/account/api-keys">https://platform.openai.com/account/api-keys</a>. Simply enter your API Key into the app and you are ready to start using ResumeGPT.</p>
19
+ <p>For any issues, please send an email to [email protected].</p>
20
+ <p>Start using ResumeGPT and improve your chances of landing a job!</p>
21
+ </body>
22
+ </html>
app.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ revChatGPT==1.1.3
2
+ pypdf2==3.0.1
3
+ Jinja2==3.1.2
4
+ pdfkit==1.0.0
5
+ streamlit==1.15.2
src/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # from .dependency_downloader import download_spacy_model, download_nltk_corpus
2
+ #
3
+ # download_spacy_model('en_core_web_sm')
4
+ # download_nltk_corpus('words')
5
+ # download_nltk_corpus('stopwords')
src/chatbot/__init__.py ADDED
File without changes
src/chatbot/chatgpt.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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):
21
+ self.chatbot = Chatbot(api_key)
22
+ logging.info("API key loaded successfully")
23
+
24
+ @staticmethod
25
+ def validate_api(api_key):
26
+ if api_key and api_key.startswith("sk-") and len(api_key) > 50:
27
+ response = requests.get("https://api.openai.com/v1/engines", headers={"Authorization": f"Bearer {api_key}"})
28
+ return response.status_code == 200
29
+ return False
30
+
31
+ @staticmethod
32
+ def load_api_key(config_path):
33
+ """
34
+ Load api key from config.json
35
+
36
+ Returns:
37
+ Str: session token
38
+ """
39
+ config_file = Path(config_path)
40
+ if not config_file.is_file():
41
+ raise FileNotFoundError(f"config.json not found at {config_file.resolve()}")
42
+
43
+ with open(config_file, 'r') as j_file:
44
+ session_token = json.load(j_file)
45
+ return session_token['api_key']
46
+
47
+ def improve_resume(self, parsed_resume: str) -> Dict:
48
+ logging.info("Improving parsed resume")
49
+ chatgpt_input = get_prompt(parsed_resume, user_request='', output_type='all')
50
+ response = self._ask(chatgpt_input)
51
+ new_resume_data = self.parse_json_from_string(response)
52
+ logging.info("Resume improved successfully")
53
+ return new_resume_data
54
+
55
+ def improve_section(self, section_text, user_request=''):
56
+ logging.info("Improving section")
57
+ chatgpt_input = get_prompt(section_text, user_request=user_request, output_type='section')
58
+ response = self._ask(chatgpt_input)
59
+ new_section_text = self.clean_section_response(response)
60
+ logging.info("Section improved successfully")
61
+ return new_section_text
62
+
63
+ def _ask(self, chatgpt_input):
64
+ logging.info("Asking chatbot for response")
65
+ try:
66
+ response = self.chatbot.ask(chatgpt_input)
67
+ answer = response['choices'][0]['text']
68
+ logging.info("Received response from chatbot")
69
+ logging.info(f"Response: {answer}")
70
+ except Exception:
71
+ answer = ""
72
+ return answer
73
+
74
+ def parse_json_from_string(self, json_string):
75
+
76
+ try:
77
+ return ast.literal_eval(json_string)
78
+ except Exception:
79
+ logging.error("Error in parsing JSON string")
80
+
81
+ json_string = re.sub('\s+', ' ', json_string)
82
+ json_string = re.sub('"', "'", json_string)
83
+ json_string = re.sub(r"(\w)'(\w)", r"\1\'\2", json_string)
84
+
85
+ clean_dict = dict()
86
+ for key, value in data_format.items():
87
+ pattern = ''
88
+ if isinstance(value, str):
89
+ pattern = f"'{key}':" + "\s*'(.*?)'"
90
+ elif isinstance(value, list):
91
+ pattern = f"'{key}':\s*(\[[^\[\]]*?\])"
92
+ elif isinstance(value, dict):
93
+ pattern = f"'{key}':" + "\s*(\{[^{}]*?\})"
94
+
95
+ extracted_value = self.extract_value(pattern, json_string)
96
+
97
+ if extracted_value:
98
+ try:
99
+ extracted_value = ast.literal_eval(extracted_value)
100
+ except Exception:
101
+ pass
102
+
103
+ if not isinstance(extracted_value, type(value)):
104
+ extracted_value = data_format[key]
105
+ clean_dict[key] = extracted_value
106
+
107
+ return clean_dict
108
+
109
+ def extract_value(self, pattern, string):
110
+ match = re.search(pattern, string)
111
+
112
+ if match:
113
+ return match.group(1)
114
+ else:
115
+ return ''
116
+
117
+ def clean_section_response(self, input_string):
118
+ try:
119
+ input_string = re.sub('^\W*"', "", input_string)
120
+ input_string = re.sub('"\W*$', "", input_string)
121
+ except ValueError:
122
+ pass
123
+ input_string = self.remove_prefix(input_string)
124
+ return input_string
125
+
126
+ @staticmethod
127
+ def remove_prefix(input_string):
128
+ return re.sub(r'\w+:\n', '', input_string)
src/chatbot/prompts.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ prompt_placeholder = '[$$$]'
2
+
3
+ data_format = {'name': '', 'title': '',
4
+ 'contactInfo': {'linkedin': '', 'github': '', 'email': '', 'address': '', 'phone': ''}, 'summary': '',
5
+ 'workExperience': [{'title': '', 'company': '', 'dates': '', 'description': ''}, ],
6
+ 'education': [{'degree': '', 'school': '', 'dates': '', 'description': ''}, ], 'skills': ['', ]}
7
+
8
+ recruiter_prompt = 'You are a professional resume builder and a recruiter.\n'
9
+ command_prompt = 'Re-write the input as professionally as possible, adding vital, valuable information and skills.\n' \
10
+ 'Enhance the input to showcase the relevant education, experience, and skills in a professional manner to effectively demonstrate value to potential employers.\n' \
11
+ f'Do it for every value in your output {str(list(data_format))}. '
12
+ user_request_prompt = f'{prompt_placeholder}'
13
+
14
+ output_commands_prompts = dict()
15
+ output_commands_prompts[
16
+ 'all'] = f'Return the output as dictionary in the next format {str(data_format)}. Return only the keys: {str(list(data_format))}.'
17
+ output_commands_prompts['section'] = f'Return the output as string.'
18
+
19
+ input_prompt = f'Input: {prompt_placeholder}'
20
+
21
+
22
+ def get_prompt(input_data, user_request='', output_type='all'):
23
+ input_data = str(input_data)
24
+ valid_output_types = list(output_commands_prompts)
25
+ assert str(output_type) in valid_output_types, f"Not valid output type, try {valid_output_types}"
26
+
27
+ if user_request:
28
+ user_request += '\n'
29
+
30
+ template = '\n'.join(
31
+ [recruiter_prompt, command_prompt, user_request_prompt.replace(prompt_placeholder, user_request),
32
+ input_prompt.replace(prompt_placeholder, input_data), output_commands_prompts[output_type], command_prompt])
33
+ return template
src/css/main.css ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+
5
+ @page {
6
+ size: A4 portrait;
7
+
8
+ }
9
+ @media print {
10
+ header, footer {
11
+ display: none;
12
+ }
13
+ }
14
+ *, *:before, *:after {
15
+ -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;
16
+ }
17
+
18
+ html
19
+ {
20
+
21
+ font-size:100%;
22
+ }
23
+
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;
32
+ font-size:18px;
33
+ font-weight:500;
34
+ line-height:1.4;
35
+ text-rendering:optimizeLegibility;
36
+ }
37
+
38
+ .parent
39
+ {
40
+ }
41
+
42
+ .skill-set li:hover
43
+ {
44
+ background:#3498db;
45
+ }
46
+
47
+ h1
48
+ {
49
+ color: #145DA0;
50
+ }
51
+
52
+ .wrapper
53
+ {
54
+ height:100%;
55
+ }
56
+
57
+
58
+
59
+
60
+ .left
61
+ {
62
+ font-family:Open Sans, sans-serif;
63
+
64
+ left: 0px;
65
+ height:100%;
66
+ margin-left:-1px;
67
+ min-width:256px;
68
+ width:70%;
69
+ float:left;
70
+ }
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%;
84
+ padding-left: 30px;
85
+ padding-top: 70px;
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
+ }
121
+
122
+
123
+ .name-hero h1
124
+ {
125
+ font-family:Open Sans, sans-serif;
126
+ font-size:1.5em;
127
+ text-align:center;
128
+ }
129
+
130
+ .name-hero h2
131
+ {
132
+ font-family:Open Sans, sans-serif;
133
+ font-size:1.5em;
134
+ text-align:center;
135
+ }
136
+
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
171
+ {
172
+ color:rgba(0,0,0,1);
173
+ font-style:normal;
174
+ font-weight:700;
175
+ }
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;
183
+ margin:0 8px 0 0;
184
+ text-align:center;
185
+ }
186
+
187
+ .name-hero .name-text
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;
201
+ max-width:975px;
202
+ padding:0em;
203
+ }
204
+
205
+ .inner h1
206
+ {
207
+ font-size:1.75em;
208
+ }
209
+
210
+ .inner p
211
+ {
212
+ color:rgba(0,0,0,1);
213
+ }
214
+
215
+ .inner p em
216
+ {
217
+ color: #145DA0;
218
+ font-style:normal;
219
+ }
220
+
221
+ .inner section
222
+ {
223
+ margin:1px auto;
224
+ }
225
+
226
+ ul
227
+ {
228
+ list-style-type:none;
229
+ margin-top:0 px;
230
+ max-width:570px;
231
+ padding:1;
232
+ }
233
+
234
+ .skill-set
235
+ {
236
+ font-family:Open Sans, sans-serif;
237
+
238
+ color:rgba(0,0,0,1);
239
+ list-style:none;
240
+ margin:1px 1px 0 0;
241
+ padding:10px;
242
+ text-align:justify;
243
+ }
244
+
src/data_handler.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ def download_pdf():
27
+ if st.session_state.get('name'):
28
+ resume_data = format_resume_data()
29
+ else:
30
+ resume_data = st.session_state['resume_data']
31
+ html_resume = build_html_resume(resume_data)
32
+ options = {'page-size': 'A4', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in',
33
+ 'margin-left': '0.75in', 'encoding': "UTF-8", 'no-outline': None}
34
+ return pdfkit.from_string(html_resume, options=options, css='src/css/main.css')
35
+
36
+
37
+ def improve_resume(resume_data=None):
38
+ if resume_data is not None:
39
+ st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(resume_data)
40
+ else:
41
+ st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(st.session_state['resume_data'])
42
+
43
+
44
+ def format_resume_data():
45
+ current_state = st.session_state
46
+ resume_data = {}
47
+ contact_info = {}
48
+ work_experience = []
49
+ education = []
50
+ skills = []
51
+
52
+ resume_data['name'] = current_state.get('name', '')
53
+ resume_data['title'] = current_state.get('title', '')
54
+
55
+ contact_info_keys = ['linkedin', 'github', 'email', 'address']
56
+ for key in contact_info_keys:
57
+ contact_info[key] = current_state.get(f'contactInfo_{key}', '')
58
+
59
+ resume_data['contactInfo'] = contact_info
60
+
61
+ resume_data['summary'] = current_state.get('summary', '')
62
+
63
+ work_experience_keys = ['workExperience_{}_title', 'workExperience_{}_company', 'workExperience_{}_dates',
64
+ 'workExperience_{}_description']
65
+ education_keys = ['education_{}_degree', 'education_{}_school', 'education_{}_dates', 'education_{}_description']
66
+
67
+ total_work_experience = count_entries(st.session_state, 'workExperience')
68
+ total_education = count_entries(st.session_state, 'education')
69
+
70
+ for i in range(total_work_experience):
71
+ work_experience.append(
72
+ {key.split('_')[2]: current_state.get(key.format(i), '') for key in work_experience_keys})
73
+
74
+ for i in range(total_education):
75
+ education.append({key.split('_')[2]: current_state.get(key.format(i), '') for key in education_keys})
76
+
77
+ resume_data['workExperience'] = work_experience
78
+ resume_data['education'] = education
79
+
80
+ total_skills = count_entries(st.session_state, 'skills')
81
+
82
+ for i in range(total_skills):
83
+ skill_key = f'skills_{i}'
84
+
85
+ skills.append(current_state.get(skill_key, ''))
86
+ resume_data['skills'] = skills
87
+ 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 ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+
3
+ <body>
4
+ <div class='parent'>
5
+ <div class="left">
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
+
19
+ <div class="summary">
20
+ <h1>Summary</h1>
21
+ <p>{{summary}}</p>
22
+ </div>
23
+ <section>
24
+ <h1>Employment</h1>
25
+ {% for experience in workExperience %}
26
+ <p><em>{{experience.dates}} | {{experience.company}} | {{experience.title}}</em></p>
27
+ <p>{{experience.description}}</p>
28
+ {% endfor %}
29
+ </section>
30
+ <section>
31
+ <h1>Education</h1>
32
+ {% for edu in education %}
33
+ <p><em>{{edu.dates}} | {{edu.school}} | {{edu.degree}}</em></p>
34
+ <p>{{edu.description}}</p>
35
+
36
+ {% endfor %}
37
+ </section>
38
+
39
+
40
+ </div>
41
+ </div>
42
+ <div class="right">
43
+ <h2>Contact Information</h2>
44
+ <p>Email: {{contactInfo.email}}</p>
45
+ <p>Phone: {{contactInfo.phone}}</p>
46
+ <p>Address: {{contactInfo.address}}</p>
47
+ <p>LinkedIn: <a href="{{contactInfo.linkedin}}">LinkedIn Profile</a></p>
48
+ <p>Github: <a href="{{contactInfo.github}}">GitHub Profile</a></p>
49
+
50
+ <section>
51
+ <h1>Skills</h1>
52
+ <ul class="skill-set">
53
+ {% for skill in skills %}
54
+ <li>{{skill}}</li>
55
+ {% endfor %}
56
+ </ul>
57
+ </section>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </body>
62
+ </html>
src/ui.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ from src.chatbot.chatgpt import openai_key_info, Chatgpt
4
+ from src.chatbot.prompts import data_format
5
+ from src.data_handler import improve_resume, init_resume, download_pdf, update_resume_data, PDFSizeException
6
+ from src.exceptions import ChatbotInitException
7
+ from src.utils import is_new_file, is_data_loaded, key_to_tab_name, get_item_key, init_user_info
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
+ 'contactInfo': 'phone, Linkedin, etc.'}
13
+
14
+
15
+ def title():
16
+ st.title("ChatCV - AI Resume Builder")
17
+
18
+
19
+ def resume_header():
20
+ st.text_input('name', st.session_state.resume_data['name'], key="name")
21
+ st.text_input('title', st.session_state.resume_data['title'], key="title")
22
+
23
+
24
+ def unknown_error():
25
+ st.session_state['user_info'] = init_user_info(error_info, "It's just a glitch in the matrix."
26
+ " Try hitting refresh, and if that doesn't work, just imagine yourself in a peaceful place.")
27
+ user_info()
28
+
29
+
30
+ def user_info():
31
+ if not st.session_state.get('user_info'):
32
+ upload_resume_header()
33
+
34
+ message_type = st.session_state['user_info']['message_type']
35
+ message = st.session_state['user_info']['message']
36
+ message_type(message)
37
+
38
+
39
+ def upload_resume_header():
40
+ st.session_state['user_info'] = init_user_info(st.success, "Upload PDF Resume - Let the magic begin. \n\n"
41
+ "This may take a bit... Grub a warm cup of coffee while we working :)")
42
+
43
+
44
+ def upload(uploaded_file):
45
+ try:
46
+ resume_data = init_resume(uploaded_file)
47
+ st.session_state['user_info'] = init_user_info(success_info, "Working on it...")
48
+ improve_resume(resume_data)
49
+
50
+ except PDFSizeException:
51
+ st.session_state['user_info'] = init_user_info(error_info, "PDF size max size is 4, try upload again...")
52
+
53
+ except Exception:
54
+ st.session_state['user_info'] = init_user_info(error_info, "PDF upload, try upload again...")
55
+
56
+
57
+ def sidebar():
58
+ with st.sidebar:
59
+ uploaded_file = st.file_uploader('Upload PDF Resume', type=["PDF"])
60
+ if uploaded_file and is_new_file(uploaded_file):
61
+ upload(uploaded_file)
62
+ st.experimental_rerun()
63
+
64
+ if is_data_loaded():
65
+ st.button("Improve More", on_click=improve_resume)
66
+ st.download_button('Download PDF', file_name='out.pdf', mime="application/json", data=download_pdf())
67
+
68
+
69
+ def body():
70
+ section_dict = {'contactInfo': contact_info_section, 'summary': summary_section, 'workExperience': list_section,
71
+ 'education': list_section, 'skills': skills_section}
72
+ tabs_names = [key_to_tab_name(key) for key in section_dict.keys()]
73
+ tabs = st.tabs(tabs_names)
74
+ for tab, key in zip(tabs, section_dict):
75
+ section_func = section_dict[key]
76
+ with tab:
77
+ section_func(key, st.session_state['resume_data'][key])
78
+
79
+
80
+ def init_chatbot():
81
+ cols = st.columns([6, 1, 1])
82
+ api_key = cols[0].text_input("Enter OpenAI API key")
83
+ cols[1].markdown("#")
84
+ api_submit = cols[1].button("Submit")
85
+
86
+ cols[2].markdown("#")
87
+ get_info = cols[2].button("Get key")
88
+ if get_info:
89
+ st.info(f"Get your key at: {openai_key_info}")
90
+ if api_submit:
91
+ if Chatgpt.validate_api(api_key):
92
+ try:
93
+ st.session_state['chatbot'] = Chatgpt(api_key)
94
+ except ChatbotInitException:
95
+ st.session_state['user_info'] = init_user_info(error_info,
96
+ "Error with Chatbot loadin, please refresh...")
97
+
98
+ st.experimental_rerun()
99
+
100
+ else:
101
+ st.error("Not valid API key - try again...")
102
+
103
+
104
+ def summary_section(section_name, summary_data):
105
+ st.text_area(section_name, summary_data, key=f'{section_name}', label_visibility='hidden')
106
+ recruiter_subsection(section_name, section_examples[section_name])
107
+
108
+
109
+ def list_section(section_name, section_data):
110
+ description_key = 'description'
111
+
112
+ item_keys = list(section_data[0].keys())
113
+ item_keys.remove(description_key)
114
+ for item_id, section_item in enumerate(section_data):
115
+
116
+ cols = st.columns(len(item_keys))
117
+ for col, key in zip(cols, item_keys):
118
+ col.text_input(key, section_item[key], key=f'{section_name}_{item_id}_{key}')
119
+ st.text_area(description_key, section_item[description_key], key=f'{section_name}_{item_id}_{description_key}')
120
+
121
+ recruiter_subsection(section_name, section_example=section_examples[section_name], item_id=item_id)
122
+ edit_list_subsection(section_name, section_data, item_id)
123
+
124
+ st.markdown('***')
125
+
126
+
127
+ def edit_list_subsection(section_name, section_data, item_id):
128
+ with st.container():
129
+ st.markdown(
130
+ """<style>
131
+ .element-container:nth-of-type(1) button {
132
+ width: 100%;
133
+ }
134
+ </style>""",
135
+ unsafe_allow_html=True,
136
+ )
137
+
138
+ remove_col, add_col = st.columns(2)
139
+ if remove_col.button('Delete', key=f'{section_name}_{item_id}_remove_from_list') and len(section_data) > 1:
140
+ del section_data[item_id]
141
+ st.experimental_rerun()
142
+
143
+ if add_col.button('Add', key=f'{section_name}_{item_id}_add_to_list') and len(section_data) < 10:
144
+ section_data.append(data_format[section_name][0])
145
+ st.experimental_rerun()
146
+
147
+
148
+ def recruiter_subsection(section_name, section_example, item_id=0):
149
+ with st.container():
150
+ cols = st.columns([3, 10], gap='small')
151
+ cols[0].write('\n')
152
+ cols[0].write('\n')
153
+ button_clicked = cols[0].button("Auto Section Improve", key=f'{section_name}_{item_id}_improve_auto')
154
+ trigger_key = 'Add a special request'
155
+ user_request_template = f"{trigger_key} to the bot here... e.g. {section_example}."
156
+
157
+ user_request = cols[1].text_input("section_example", value=user_request_template,
158
+ key=f'{section_name}_{item_id}_improve_manual', label_visibility='hidden')
159
+ if button_clicked:
160
+ user_request = '' if trigger_key in user_request else user_request
161
+ section_key = get_item_key(section_name, item_id)
162
+ section_text = st.session_state[section_key]
163
+ new_section_text = st.session_state['chatbot'].improve_section(section_text, user_request)
164
+
165
+ update_resume_data(new_section_text, section_name, item_id)
166
+ st.experimental_rerun()
167
+
168
+
169
+ def skills_section(section_name, skills_data):
170
+ [skills_data.remove(skill) for skill in skills_data if not skill]
171
+
172
+ num_columns = 3
173
+ for skills_row in range(0, len(skills_data), num_columns):
174
+ cols = st.columns([3, 1] * num_columns)
175
+ skills_row_names = skills_data[skills_row: skills_row + num_columns]
176
+ for item_id, skill in enumerate(skills_row_names):
177
+ skill_id = skills_row + item_id
178
+ cols[item_id * 2].text_input(' ', value=skill, key=f'{section_name}_{skill_id}', label_visibility='hidden')
179
+ cols[item_id * 2 + 1].markdown('## ')
180
+ if cols[item_id * 2 + 1].button('x', key=f'{section_name}_{skill_id}_remove_from_list'):
181
+ del skills_data[skill_id]
182
+ st.experimental_rerun()
183
+
184
+ skill_subsection(section_name)
185
+ st.markdown('***')
186
+
187
+
188
+ def skill_subsection(section_name, item_id=0):
189
+ key = f'{section_name}_{item_id}_add_skill'
190
+ cols = st.columns([12, 1])
191
+ new_skill = cols[0].text_input("Add skill", key=key)
192
+ cols[1].markdown('##')
193
+ clicked = cols[1].button("\+")
194
+ if clicked and new_skill:
195
+ st.session_state['resume_data'][section_name].append(new_skill)
196
+ st.experimental_rerun()
197
+
198
+
199
+ def contact_info_section(section_name, info_data):
200
+ keys = sorted(info_data.keys())
201
+ for key in keys:
202
+ value = info_data[key]
203
+ cols = st.columns([12, 1])
204
+ cols[0].text_input(key.title(), value, key=f'{section_name}_{key}')
205
+ cols[1].markdown('##')
206
+ clicked = cols[1].button('\-', key=f'{section_name}_{key}_remove')
207
+ if clicked:
208
+ del info_data[key]
209
+ st.experimental_rerun()
210
+
211
+ add_contact_subsection(section_name, info_data)
212
+ st.markdown('***')
213
+
214
+
215
+ def add_contact_subsection(section_name, info_data):
216
+ st.markdown('***')
217
+
218
+ with st.container():
219
+ cols = st.columns([12, 1])
220
+ new_key = cols[0].text_input('Add new details', value=f"e.g, {section_examples[section_name]}")
221
+ cols[1].markdown('##')
222
+ clicked = cols[1].button('\+', key=f'{section_name}_add_details')
223
+
224
+ if clicked and new_key:
225
+ info_data[new_key] = ''
226
+ st.experimental_rerun()
227
+ # if remove_col.button('Delete', key=f'{section_name}_{item_id}_remove_from_list') and len(section_data) > 1:
228
+ # del section_data[item_id]
229
+ # st.experimental_rerun()
230
+ #
231
+ # if add_col.button('Add', key=f'{section_name}_{item_id}_add_to_list') and len(section_data) < 10:
232
+ # section_data.append(data_format[section_name][0])
233
+ # st.experimental_rerun()
234
+
235
+
236
+ def success_info(message):
237
+ st.success(message)
238
+
239
+
240
+ def error_info(message):
241
+ st.error(message)
src/utils.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }