Chand11 commited on
Commit
6c60829
Β·
verified Β·
1 Parent(s): 0791598

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -241
app.py CHANGED
@@ -1,5 +1,3 @@
1
- import os
2
- import pathlib
3
  import json
4
  import PyPDF2
5
  import docx
@@ -12,41 +10,34 @@ from reportlab.lib.pagesizes import letter
12
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
13
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
 
15
- # GOOGLE GEMINI SDK
16
- import google.generativeai as genai
17
 
18
- # --- BEGIN: Safe Streamlit paths (prevents '/.streamlit' PermissionError on Spaces) ---
19
- PROJECT_ROOT = str(pathlib.Path(__file__).parent.resolve())
20
- os.environ["HOME"] = PROJECT_ROOT
21
- os.makedirs(os.path.join(PROJECT_ROOT, ".streamlit"), exist_ok=True)
22
- os.environ["STREAMLIT_CACHE_DIR"] = os.path.join(PROJECT_ROOT, ".streamlit", "cache")
23
- os.makedirs(os.environ["STREAMLIT_CACHE_DIR"], exist_ok=True)
24
- # --- END: Safe Streamlit paths ---
25
-
26
- # ---------- File utilities ----------
27
  def read_pdf(file):
28
- reader = PyPDF2.PdfReader(file)
29
  text = ""
30
- for page in reader.pages:
31
- text += (page.extract_text() or "")
32
  return text
33
 
 
34
  def read_docx(file):
35
- d = docx.Document(file)
36
- return "\n".join(p.text for p in d.paragraphs)
 
 
 
 
37
 
38
  def load_resume(uploaded_file):
39
- if uploaded_file is None:
40
- return None
41
- name = uploaded_file.name.lower()
42
- if name.endswith(".pdf"):
43
  return read_pdf(uploaded_file)
44
- if name.endswith(".docx"):
45
  return read_docx(uploaded_file)
46
- st.error("Unsupported file format. Please upload PDF or DOCX.")
47
- return None
 
48
 
49
- # ---------- Resume PDF builder ----------
50
  def generate_updated_resume(resume_text, match_analysis):
51
  buffer = BytesIO()
52
  doc = SimpleDocTemplate(buffer, pagesize=letter,
@@ -54,208 +45,319 @@ def generate_updated_resume(resume_text, match_analysis):
54
  topMargin=60, bottomMargin=40)
55
  styles = getSampleStyleSheet()
56
 
 
57
  header_style = styles['Heading1']
58
  header_style.fontSize = 16
59
  header_style.spaceAfter = 18
60
  header_style.textColor = colors.HexColor('#1a1a1a')
61
 
62
  section_header_style = ParagraphStyle(
63
- name='SectionHeader', parent=styles['Heading2'],
64
- fontSize=13, spaceAfter=12, textColor=colors.HexColor('#0d47a1'),
65
- underlineWidth=1, underlineOffset=-3
 
 
 
 
66
  )
67
 
68
  normal_style = ParagraphStyle(
69
- name='NormalText', parent=styles['Normal'],
70
- fontSize=10, leading=14, spaceAfter=6,
 
 
 
71
  )
 
72
  bullet_style = ParagraphStyle(
73
- name='BulletStyle', parent=normal_style,
74
- bulletFontName='Helvetica', bulletFontSize=8,
75
- bulletIndent=10, leftIndent=20
 
 
 
 
 
 
 
 
 
 
 
 
76
  )
77
 
78
  content = []
79
  content.append(Paragraph("Updated Resume", header_style))
80
  content.append(Spacer(1, 12))
81
 
82
- common_sections = ['EXPERIENCE','EDUCATION','SKILLS','PROJECTS','CERTIFICATIONS','SUMMARY','OBJECTIVE']
 
 
83
  bullets = []
84
 
85
  def flush_bullets():
86
- for b in bullets:
87
- if b.strip():
88
- content.append(Paragraph("β€’ " + b.strip(), bullet_style))
89
  bullets.clear()
90
 
91
- for line in (resume_text or "").splitlines():
92
- line = (line or "").strip()
 
 
93
  if not line:
94
  continue
95
- is_section = line.isupper() or any(s in line.upper() for s in common_sections)
 
 
96
  if is_section:
97
  flush_bullets()
 
98
  content.append(Spacer(1, 12))
99
- content.append(Paragraph(line, section_header_style))
100
  else:
101
  bullets.append(line)
 
102
  flush_bullets()
103
 
104
- # ATS suggestions section
105
  if match_analysis.get('ats_optimization_suggestions'):
106
  content.append(Spacer(1, 20))
107
  content.append(Paragraph("ATS Optimization Recommendations", section_header_style))
108
  content.append(Spacer(1, 10))
109
- for s in match_analysis['ats_optimization_suggestions']:
110
- section = s.get('section','')
111
- current = s.get('current_content','')
112
- change = s.get('suggested_change','')
113
- keywords = ', '.join(s.get('keywords_to_add',[]) or [])
114
- formatting = s.get('formatting_suggestion','')
115
- reason = s.get('reason','')
116
-
117
- content.append(Paragraph(f"β€’ Section: {section}", bullet_style))
118
  if current:
119
- content.append(Paragraph(f" Current: {current}", bullet_style))
120
- content.append(Paragraph(f" Suggestion: {change}", bullet_style))
121
  if keywords:
122
- content.append(Paragraph(f" Keywords to Add: {keywords}", bullet_style))
123
  if formatting:
124
- content.append(Paragraph(f" Formatting: {formatting}", bullet_style))
125
  if reason:
126
- content.append(Paragraph(f" Reason: {reason}", bullet_style))
127
  content.append(Spacer(1, 6))
128
 
129
  doc.build(content)
130
  buffer.seek(0)
131
  return buffer
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- # ---------- Gemini helpers ----------
135
- def _configure_model(api_key: str):
136
- genai.configure(api_key=api_key)
137
- return genai.GenerativeModel("gemini-1.5-flash")
138
 
139
- def _parse_json_or_show(raw_text: str) -> dict:
140
- """Try JSON parse; if fails, show the raw output and propagate an empty dict."""
141
- if raw_text is None:
142
- raise ValueError("Empty response from model.")
143
- txt = raw_text.strip()
144
- try:
145
- return json.loads(txt)
146
- except Exception:
147
- st.error("The model did not return valid JSON. Raw output shown below to help you debug:")
148
- st.code(raw_text)
149
- return {}
150
 
151
  class JobAnalyzer:
152
  def __init__(self, api_key: str):
153
- self.model = _configure_model(api_key)
 
 
154
 
155
  def analyze_job(self, job_description: str) -> dict:
156
- prompt = f"""
157
- Return ONLY valid JSON for this job description with keys:
158
- 1) "key_technical_skills"
159
- 2) "soft_skills"
160
- 3) "years_experience"
161
- 4) "education_requirements"
162
- 5) "key_responsibilities"
163
- 6) "company_culture_indicators"
164
- 7) "required_certifications"
165
- 8) "industry_type"
166
- 9) "job_level"
167
- 10) "key_technologies"
168
-
169
- Job Description:
170
- {job_description}
171
  """
172
  try:
173
- resp = self.model.generate_content(prompt)
174
- return _parse_json_or_show(getattr(resp, "text", None))
 
 
175
  except Exception as e:
176
- st.error(f"Error analyzing job description: {e}")
177
  return {}
178
 
179
  def analyze_resume(self, resume_text: str) -> dict:
180
- prompt = f"""
181
- Return ONLY valid JSON for this resume with keys:
182
- 1) "technical_skills"
183
- 2) "soft_skills"
184
- 3) "years_experience"
185
- 4) "education_details"
186
- 5) "key_achievements"
187
- 6) "core_competencies"
188
- 7) "industry_experience"
189
- 8) "leadership_experience"
190
- 9) "technologies_used"
191
- 10) "projects_completed"
192
-
193
- Resume:
194
- {resume_text}
195
  """
196
  try:
197
- resp = self.model.generate_content(prompt)
198
- return _parse_json_or_show(getattr(resp, "text", None))
 
 
 
 
 
 
199
  except Exception as e:
200
- st.error(f"Error analyzing resume: {e}")
201
  return {}
202
 
203
  def analyze_match(self, job_analysis: dict, resume_analysis: dict) -> dict:
204
- prompt = f"""Respond ONLY with valid JSON. Compare and return EXACT keys:
 
 
205
 
 
206
  {{
207
  "overall_match_percentage":"85%",
208
- "matching_skills":[{{"skill_name":"Python","is_match":true}}],
209
  "missing_skills":[{{"skill_name":"Docker","is_match":false,"suggestion":"Consider obtaining Docker certification"}}],
210
- "skills_gap_analysis":{{"technical_skills":"...", "soft_skills":"..."}},
211
- "experience_match_analysis":"...",
212
- "education_match_analysis":"...",
213
- "recommendations_for_improvement":[{{"recommendation":"...", "section":"...", "guidance":"..."}}],
214
- "ats_optimization_suggestions":[{{"section":"...", "current_content":"...", "suggested_change":"...", "keywords_to_add":["..."], "formatting_suggestion":"...", "reason":"..."}}],
215
- "key_strengths":"...",
216
- "areas_of_improvement":"..."
217
  }}
218
 
219
- Job Requirements:
220
- {job}
221
-
222
- Resume Details:
223
- {resume}
224
- """
225
  try:
226
- resp = self.model.generate_content(
227
  prompt.format(
228
  job=json.dumps(job_analysis, indent=2),
229
  resume=json.dumps(resume_analysis, indent=2)
230
  )
231
  )
232
- return _parse_json_or_show(getattr(resp, "text", None))
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  except Exception as e:
234
- st.error(f"Error analyzing match: {e}")
235
  return {}
236
 
237
 
238
  class CoverLetterGenerator:
239
  def __init__(self, api_key: str):
240
- self.model = _configure_model(api_key)
241
-
242
- def generate_cover_letter(self, job_analysis: dict, resume_analysis: dict, match_analysis: dict, tone: str = "professional") -> str:
243
- prompt = f"""
244
- Generate a tailored cover letter. Be concise, ATS-friendly, specific, and include a strong CTA.
245
-
246
- Job:
247
- {job_analysis}
248
-
249
- Resume:
250
- {resume_analysis}
251
-
252
- Match:
253
- {match_analysis}
254
-
255
  Tone: {tone}
 
 
 
 
 
 
 
 
 
 
256
  """
257
  try:
258
- resp = self.model.generate_content(
259
  prompt.format(
260
  job=json.dumps(job_analysis, indent=2),
261
  resume=json.dumps(resume_analysis, indent=2),
@@ -263,127 +365,157 @@ Tone: {tone}
263
  tone=tone
264
  )
265
  )
266
- return (getattr(resp, "text", None) or "").strip()
267
  except Exception as e:
268
- st.error(f"Error generating cover letter: {e}")
269
  return ""
270
 
271
 
272
- # ---------- Streamlit UI ----------
273
  def main():
274
  st.set_page_config(page_title="LinkedIn Job Application Assistant - HireReady πŸ“", layout="wide")
275
 
276
- api_key = st.sidebar.text_input("Enter Google (Gemini) API Key πŸ—οΈ", type="password")
 
277
  if not api_key:
278
- st.warning("πŸ”‘ Enter your Google Gemini API key to continue.")
279
  return
280
 
281
  st.title("LinkedIn Job Application Assistant - HireReady πŸš€")
282
- st.markdown("Analyze a job description and your resume, get a match report, a cover letter, and an updated resume.")
 
 
283
 
284
- analyzer = JobAnalyzer(api_key)
285
- letter_gen = CoverLetterGenerator(api_key)
 
 
286
 
 
287
  col1, col2 = st.columns(2)
288
  with col1:
289
  st.subheader("Job Description πŸ“‹")
290
  job_desc = st.text_area("Paste the job description here", height=300)
291
  with col2:
292
  st.subheader("Your Resume πŸ“œ")
293
- resume_file = st.file_uploader("Upload your resume (PDF/DOCX)", type=['pdf', 'docx'])
294
 
295
  if job_desc and resume_file:
296
  with st.spinner("πŸ” Analyzing your application..."):
 
297
  resume_text = load_resume(resume_file)
298
- if not resume_text:
299
- st.error("Couldn't read the resume file.")
300
- return
301
-
302
- job_analysis = analyzer.analyze_job(job_desc)
303
- resume_analysis = analyzer.analyze_resume(resume_text)
304
- match_analysis = analyzer.analyze_match(job_analysis, resume_analysis)
305
-
306
- if not job_analysis or not resume_analysis or not match_analysis:
307
- st.error("Insufficient data returned from the model.")
308
- return
309
-
310
- st.header("Analysis Results πŸ“Š")
311
- c1, c2, c3 = st.columns(3)
312
- with c1:
313
- st.metric("Overall Match 🎯", f"{match_analysis.get('overall_match_percentage', '0%')}")
314
- with c2:
315
- st.metric("Skills Match 🧠", f"{len(match_analysis.get('matching_skills', []))} skills")
316
- with c3:
317
- st.metric("Skills to Develop πŸ“ˆ", f"{len(match_analysis.get('missing_skills', []))} skills")
318
-
319
- tab1, tab2, tab3, tab4, tab5 = st.tabs([
320
- "Skills Analysis πŸ“Š", "Experience Match πŸ—‚οΈ", "Recommendations πŸ’‘", "Cover Letter πŸ’Œ", "Updated Resume πŸ“"
321
- ])
322
-
323
- with tab1:
324
- st.subheader("Matching Skills")
325
- for s in match_analysis.get('matching_skills', []):
326
- st.success(f"βœ… {s.get('skill_name','')}")
327
-
328
- st.subheader("Missing Skills")
329
- for s in match_analysis.get('missing_skills', []):
330
- st.warning(f"⚠️ {s.get('skill_name','')}")
331
- if s.get('suggestion'):
332
- st.info(f"Suggestion: {s['suggestion']}")
333
-
334
- data = pd.DataFrame({
335
- 'Status': ['Matching', 'Missing'],
336
- 'Count': [len(match_analysis.get('matching_skills', [])),
337
- len(match_analysis.get('missing_skills', []))]
338
- })
339
- fig = px.bar(data, x='Status', y='Count', color='Status',
340
- color_discrete_sequence=['#5cb85c', '#d9534f'],
341
- title='Skills Analysis')
342
- fig.update_layout(xaxis_title='Status', yaxis_title='Count')
343
- st.plotly_chart(fig)
344
-
345
- with tab2:
346
- st.write("### Experience Match Analysis πŸ—‚οΈ")
347
- st.write(match_analysis.get('experience_match_analysis', ''))
348
- st.write("### Education Match Analysis πŸŽ“")
349
- st.write(match_analysis.get('education_match_analysis', ''))
350
-
351
- with tab3:
352
- st.write("### Key Recommendations πŸ”‘")
353
- for rec in match_analysis.get('recommendations_for_improvement', []):
354
- st.info(f"**{rec.get('recommendation','')}**")
355
- st.write(f"**Section:** {rec.get('section','')}")
356
- st.write(f"**Guidance:** {rec.get('guidance','')}")
357
-
358
- st.write("### ATS Optimization Suggestions πŸ€–")
359
- for sug in match_analysis.get('ats_optimization_suggestions', []):
360
- st.write("---")
361
- st.warning(f"**Section to Modify:** {sug.get('section','')}")
362
- if sug.get('current_content'):
363
- st.write(f"**Current Content:** {sug['current_content']}")
364
- st.write(f"**Suggested Change:** {sug.get('suggested_change','')}")
365
- if sug.get('keywords_to_add'):
366
- st.write(f"**Keywords to Add:** {', '.join(sug['keywords_to_add'])}")
367
- if sug.get('formatting_suggestion'):
368
- st.write(f"**Formatting Changes:** {sug['formatting_suggestion']}")
369
- if sug.get('reason'):
370
- st.info(f"**Reason for Change:** {sug['reason']}")
371
-
372
- with tab4:
373
- st.write("### Cover Letter Generator πŸ–ŠοΈ")
374
- tone = st.selectbox("Select tone 🎭", ["Professional πŸ‘”", "Enthusiastic πŸ˜ƒ", "Confident 😎", "Friendly πŸ‘‹"])
375
- if st.button("Generate Cover Letter ✍️"):
376
- with st.spinner("✍️ Crafting your cover letter..."):
377
- cover = letter_gen.generate_cover_letter(job_analysis, resume_analysis, match_analysis, tone.lower().split()[0])
378
- st.markdown("### Your Custom Cover Letter πŸ’Œ")
379
- st.text_area("", cover, height=400)
380
- st.download_button("Download Cover Letter πŸ“₯", cover, "cover_letter.txt", "text/plain")
381
-
382
- with tab5:
383
- st.write("### Updated Resume πŸ“")
384
- updated_pdf = generate_updated_resume(resume_text, match_analysis)
385
- st.download_button("Download Updated Resume πŸ“₯", updated_pdf, "updated_resume.pdf", mime="application/pdf")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
 
388
  if __name__ == "__main__":
389
- main()
 
 
 
1
  import json
2
  import PyPDF2
3
  import docx
 
10
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
11
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
12
 
13
+ import google.generativeai as genai # Replaced openai import
 
14
 
15
+ # Utility Functions
 
 
 
 
 
 
 
 
16
  def read_pdf(file):
17
+ pdf_reader = PyPDF2.PdfReader(file)
18
  text = ""
19
+ for page in pdf_reader.pages:
20
+ text += page.extract_text()
21
  return text
22
 
23
+
24
  def read_docx(file):
25
+ doc = docx.Document(file)
26
+ text = ""
27
+ for paragraph in doc.paragraphs:
28
+ text += paragraph.text + "\n"
29
+ return text
30
+
31
 
32
  def load_resume(uploaded_file):
33
+ if uploaded_file.name.endswith('.pdf'):
 
 
 
34
  return read_pdf(uploaded_file)
35
+ elif uploaded_file.name.endswith('.docx'):
36
  return read_docx(uploaded_file)
37
+ else:
38
+ st.error("Unsupported file format")
39
+ return None
40
 
 
41
  def generate_updated_resume(resume_text, match_analysis):
42
  buffer = BytesIO()
43
  doc = SimpleDocTemplate(buffer, pagesize=letter,
 
45
  topMargin=60, bottomMargin=40)
46
  styles = getSampleStyleSheet()
47
 
48
+ # Custom styles
49
  header_style = styles['Heading1']
50
  header_style.fontSize = 16
51
  header_style.spaceAfter = 18
52
  header_style.textColor = colors.HexColor('#1a1a1a')
53
 
54
  section_header_style = ParagraphStyle(
55
+ name='SectionHeader',
56
+ parent=styles['Heading2'],
57
+ fontSize=13,
58
+ spaceAfter=12,
59
+ textColor=colors.HexColor('#0d47a1'),
60
+ underlineWidth=1,
61
+ underlineOffset=-3
62
  )
63
 
64
  normal_style = ParagraphStyle(
65
+ name='NormalText',
66
+ parent=styles['Normal'],
67
+ fontSize=10,
68
+ leading=14,
69
+ spaceAfter=6,
70
  )
71
+
72
  bullet_style = ParagraphStyle(
73
+ name='BulletStyle',
74
+ parent=normal_style,
75
+ bulletFontName='Helvetica',
76
+ bulletFontSize=8,
77
+ bulletIndent=10,
78
+ leftIndent=20
79
+ )
80
+
81
+ recommendation_style = ParagraphStyle(
82
+ name='RecommendationStyle',
83
+ parent=styles['Normal'],
84
+ fontSize=9,
85
+ textColor=colors.HexColor('#00695c'),
86
+ leftIndent=25,
87
+ spaceAfter=4
88
  )
89
 
90
  content = []
91
  content.append(Paragraph("Updated Resume", header_style))
92
  content.append(Spacer(1, 12))
93
 
94
+ # Resume Content Parsing
95
+ resume_parts = resume_text.split("\n")
96
+ current_section = ""
97
  bullets = []
98
 
99
  def flush_bullets():
100
+ for bullet in bullets:
101
+ content.append(Paragraph(f"β€’ {bullet.strip()}", bullet_style))
 
102
  bullets.clear()
103
 
104
+ common_sections = ['EXPERIENCE', 'EDUCATION', 'SKILLS', 'PROJECTS', 'CERTIFICATIONS', 'SUMMARY', 'OBJECTIVE']
105
+
106
+ for line in resume_parts:
107
+ line = line.strip()
108
  if not line:
109
  continue
110
+
111
+ is_section = line.isupper() or any(section in line.upper() for section in common_sections)
112
+
113
  if is_section:
114
  flush_bullets()
115
+ current_section = line
116
  content.append(Spacer(1, 12))
117
+ content.append(Paragraph(current_section, section_header_style))
118
  else:
119
  bullets.append(line)
120
+
121
  flush_bullets()
122
 
123
+ # ATS Recommendations
124
  if match_analysis.get('ats_optimization_suggestions'):
125
  content.append(Spacer(1, 20))
126
  content.append(Paragraph("ATS Optimization Recommendations", section_header_style))
127
  content.append(Spacer(1, 10))
128
+
129
+ for suggestion in match_analysis['ats_optimization_suggestions']:
130
+ section = suggestion.get('section', '')
131
+ current = suggestion.get('current_content', '')
132
+ suggested = suggestion.get('suggested_change', '')
133
+ keywords = ', '.join(suggestion.get('keywords_to_add', []))
134
+ formatting = suggestion.get('formatting_suggestion', '')
135
+ reason = suggestion.get('reason', '')
136
+ content.append(Paragraph(f"β€’ Section: {section}", recommendation_style))
137
  if current:
138
+ content.append(Paragraph(f" Current: {current}", recommendation_style))
139
+ content.append(Paragraph(f" Suggestion: {suggested}", recommendation_style))
140
  if keywords:
141
+ content.append(Paragraph(f" Keywords to Add: {keywords}", recommendation_style))
142
  if formatting:
143
+ content.append(Paragraph(f" Formatting: {formatting}", recommendation_style))
144
  if reason:
145
+ content.append(Paragraph(f" Reason: {reason}", recommendation_style))
146
  content.append(Spacer(1, 6))
147
 
148
  doc.build(content)
149
  buffer.seek(0)
150
  return buffer
151
 
152
+ def generate_updated_resume1(resume_text, match_analysis):
153
+ buffer = BytesIO()
154
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
155
+ styles = getSampleStyleSheet()
156
+
157
+ # Modify existing styles
158
+ styles['Heading1'].fontSize = 14
159
+ styles['Heading1'].spaceAfter = 16
160
+ styles['Heading1'].textColor = colors.HexColor('#2c3e50')
161
+ styles['Heading2'].fontSize = 12
162
+ styles['Heading2'].spaceAfter = 12
163
+ styles['Heading2'].textColor = colors.HexColor('#34495e')
164
+ styles['Normal'].fontSize = 10
165
+ styles['Normal'].spaceAfter = 8
166
+ styles['Normal'].leading = 14
167
+
168
+ # Add a custom style for recommendations
169
+ styles.add(ParagraphStyle(
170
+ name='RecommendationStyle',
171
+ parent=styles['Normal'],
172
+ fontSize=10,
173
+ spaceAfter=8,
174
+ leading=14,
175
+ leftIndent=20,
176
+ textColor=colors.HexColor('#2980b9')
177
+ ))
178
+
179
+ # Create content
180
+ content = []
181
+
182
+ # Add header
183
+ content.append(Paragraph("Updated Resume", styles['Heading1']))
184
+ content.append(Spacer(1, 12))
185
+
186
+ # Add existing resume content with proper formatting
187
+ resume_parts = resume_text.split("\n")
188
+ current_section = None
189
+ for part in resume_parts:
190
+ if part.strip(): # Skip empty lines
191
+ # Detect section headers (uppercase or common section names)
192
+ common_sections = ['EXPERIENCE', 'EDUCATION', 'SKILLS', 'PROJECTS', 'CERTIFICATIONS']
193
+ is_section = part.isupper() or any(section in part.upper() for section in common_sections)
194
+
195
+ if is_section:
196
+ current_section = part
197
+ content.append(Paragraph(part, styles['Heading2']))
198
+ else:
199
+ content.append(Paragraph(part, styles['Normal']))
200
+ content.append(Spacer(1, 6))
201
+
202
+ # Add ATS optimization recommendations
203
+ if match_analysis.get('ats_optimization_suggestions'):
204
+ content.append(Spacer(1, 12))
205
+ content.append(Paragraph("ATS Optimization Recommendations", styles['Heading2']))
206
+ content.append(Spacer(1, 8))
207
+
208
+ for suggestion in match_analysis['ats_optimization_suggestions']:
209
+ content.append(Paragraph(f"β€’ Section: {suggestion['section']}", styles['RecommendationStyle']))
210
+ if suggestion.get('current_content'):
211
+ content.append(Paragraph(f" Current: {suggestion['current_content']}", styles['RecommendationStyle']))
212
+ content.append(Paragraph(f" Suggestion: {suggestion['suggested_change']}", styles['RecommendationStyle']))
213
+ if suggestion.get('keywords_to_add'):
214
+ content.append(Paragraph(f" Keywords to Add: {', '.join(suggestion['keywords_to_add'])}", styles['RecommendationStyle']))
215
+ if suggestion.get('formatting_suggestion'):
216
+ content.append(
217
+ Paragraph(f" Formatting: {suggestion['formatting_suggestion']}", styles['RecommendationStyle']))
218
+ content.append(Spacer(1, 6))
219
 
220
+ # Build PDF
221
+ doc.build(content)
222
+ buffer.seek(0)
223
+ return buffer
224
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  class JobAnalyzer:
227
  def __init__(self, api_key: str):
228
+ # Configure Google Generative AI
229
+ genai.configure(api_key=api_key)
230
+ self.model = genai.GenerativeModel("gemini-1.5-flash") # You can choose a different model
231
 
232
  def analyze_job(self, job_description: str) -> dict:
233
+ prompt = """
234
+ Analyze this job description and provide a detailed JSON with:
235
+ 1. Key technical skills required
236
+ 2. Soft skills required
237
+ 3. Years of experience required
238
+ 4. Education requirements
239
+ 5. Key responsibilities
240
+ 6. Company culture indicators
241
+ 7. Required certifications
242
+ 8. Industry type
243
+ 9. Job level (entry, mid, senior)
244
+ 10. Key technologies mentioned
245
+
246
+ Format the response as a JSON object with these categories.
247
+ Job Description: {description}
248
  """
249
  try:
250
+ response = self.model.generate_content(prompt.format(description=job_description))
251
+ # Assuming the response text is a valid JSON string
252
+ parsed_response = json.loads(response.text)
253
+ return parsed_response
254
  except Exception as e:
255
+ st.error(f"Error analyzing job description: {str(e)}")
256
  return {}
257
 
258
  def analyze_resume(self, resume_text: str) -> dict:
259
+ prompt = """
260
+ Analyze this resume and provide a detailed JSON with:
261
+ 1. Technical skills
262
+ 2. Soft skills
263
+ 3. Years of experience
264
+ 4. Education details
265
+ 5. Key achievements
266
+ 6. Core competencies
267
+ 7. Industry experience
268
+ 8. Leadership experience
269
+ 9. Technologies used
270
+ 10. Projects completed
271
+
272
+ Format the response as a JSON object with these categories.
273
+ Resume: {resume}
274
  """
275
  try:
276
+ response = self.model.generate_content(prompt.format(resume=resume_text))
277
+ # Assuming the response text is a valid JSON string
278
+ parsed_response = json.loads(response.text)
279
+ return parsed_response
280
+ except json.JSONDecodeError as e:
281
+ st.error(
282
+ f"Error parsing resume analysis response: {str(e)}. Please check the resume text for any formatting issues.")
283
+ return {}
284
  except Exception as e:
285
+ st.error(f"Error analyzing resume: {str(e)}")
286
  return {}
287
 
288
  def analyze_match(self, job_analysis: dict, resume_analysis: dict) -> dict:
289
+ prompt = """You are a professional resume analyzer. Compare the provided job requirements and resume to generate a detailed analysis in valid JSON format. IMPORTANT: Respond ONLY with a valid JSON object and NO additional text or formatting.
290
+ Job Requirements: {job}
291
+ Resume Details: {resume}
292
 
293
+ Generate a response following this EXACT structure:
294
  {{
295
  "overall_match_percentage":"85%",
296
+ "matching_skills":[{{"skill_name":"Python","is_match":true}},{{"skill_name":"AWS","is_match":true}}],
297
  "missing_skills":[{{"skill_name":"Docker","is_match":false,"suggestion":"Consider obtaining Docker certification"}}],
298
+ "skills_gap_analysis":{{"technical_skills":"Specific technical gap analysis","soft_skills":"Specific soft skills gap analysis"}},
299
+ "experience_match_analysis":"Detailed experience match analysis",
300
+ "education_match_analysis":"Detailed education match analysis",
301
+ "recommendations_for_improvement":[{{"recommendation":"Add metrics","section":"Experience","guidance":"Quantify achievements with specific numbers"}}],
302
+ "ats_optimization_suggestions":[{{"section":"Skills","current_content":"Current format","suggested_change":"Specific change needed","keywords_to_add":["keyword1","keyword2"],"formatting_suggestion":"Specific format change","reason":"Detailed reason"}}],
303
+ "key_strengths":"Specific key strengths",
304
+ "areas_of_improvement":"Specific areas to improve"
305
  }}
306
 
307
+ Focus on providing detailed, actionable insights for each field. Keep the JSON structure exact but replace the example content with detailed analysis based on the provided job and resume."""
 
 
 
 
 
308
  try:
309
+ response = self.model.generate_content(
310
  prompt.format(
311
  job=json.dumps(job_analysis, indent=2),
312
  resume=json.dumps(resume_analysis, indent=2)
313
  )
314
  )
315
+ try:
316
+ # Clean up the response content
317
+ response_content = response.text.strip()
318
+ # Remove any leading/trailing whitespace or quotes
319
+ response_content = response_content.strip('"\'')
320
+ # Parse the JSON
321
+ parsed_response = json.loads(response_content)
322
+ return parsed_response
323
+ except json.JSONDecodeError as e:
324
+ st.error(f"Error parsing match analysis response. Please try again.")
325
+ print(f"Debug - Response content: {response.text}")
326
+ print(f"Debug - Error details: {str(e)}")
327
+ return {}
328
+ return parsed_response
329
  except Exception as e:
330
+ st.error(f"Error analyzing match: {str(e)}")
331
  return {}
332
 
333
 
334
  class CoverLetterGenerator:
335
  def __init__(self, api_key: str):
336
+ # Configure Google Generative AI
337
+ genai.configure(api_key=api_key)
338
+ self.model = genai.GenerativeModel("gemini-1.5-flash") # You can choose a different model
339
+
340
+ def generate_cover_letter(self, job_analysis: dict, resume_analysis: dict, match_analysis: dict,
341
+ tone: str = "professional") -> str:
342
+ prompt = """
343
+ Generate a compelling cover letter using this information:
344
+ Job Details: {job}
345
+ Candidate Details: {resume}
346
+ Match Analysis: {match}
 
 
 
 
347
  Tone: {tone}
348
+
349
+ Requirements:
350
+ 1. Make it personal and specific
351
+ 2. Highlight the strongest matches
352
+ 3. Address potential gaps professionally
353
+ 4. Keep it concise but impactful
354
+ 5. Use the specified tone: {tone}
355
+ 6. Include specific examples from the resume
356
+ 7. Make it ATS-friendly
357
+ 8. Add a strong call to action
358
  """
359
  try:
360
+ response = self.model.generate_content(
361
  prompt.format(
362
  job=json.dumps(job_analysis, indent=2),
363
  resume=json.dumps(resume_analysis, indent=2),
 
365
  tone=tone
366
  )
367
  )
368
+ return response.text
369
  except Exception as e:
370
+ st.error(f"Error generating cover letter: {str(e)}")
371
  return ""
372
 
373
 
 
374
  def main():
375
  st.set_page_config(page_title="LinkedIn Job Application Assistant - HireReady πŸ“", layout="wide")
376
 
377
+ # API key input
378
+ api_key = st.sidebar.text_input("Enter Google AI Studio API Key πŸ—οΈ", type="password") # Changed label
379
  if not api_key:
380
+ st.warning("πŸ”‘ Please enter your Google AI Studio API key to continue.")
381
  return
382
 
383
  st.title("LinkedIn Job Application Assistant - HireReady πŸš€")
384
+ st.markdown("""
385
+ Optimize your job application by analyzing job requirements πŸ“‹, matching your resume πŸ“œ, and generating tailored cover letters πŸ’Œ.
386
+ """)
387
 
388
+ # Initialize analyzers
389
+ # Pass the API key during initialization
390
+ job_analyzer = JobAnalyzer(api_key)
391
+ cover_letter_gen = CoverLetterGenerator(api_key)
392
 
393
+ # File Upload Section
394
  col1, col2 = st.columns(2)
395
  with col1:
396
  st.subheader("Job Description πŸ“‹")
397
  job_desc = st.text_area("Paste the job description here", height=300)
398
  with col2:
399
  st.subheader("Your Resume πŸ“œ")
400
+ resume_file = st.file_uploader("Upload your resume", type=['pdf', 'docx'])
401
 
402
  if job_desc and resume_file:
403
  with st.spinner("πŸ” Analyzing your application..."):
404
+ # Load and analyze resume
405
  resume_text = load_resume(resume_file)
406
+ if resume_text:
407
+ # Perform analysis
408
+ job_analysis = job_analyzer.analyze_job(job_desc)
409
+ resume_analysis = job_analyzer.analyze_resume(resume_text)
410
+ match_analysis = job_analyzer.analyze_match(job_analysis, resume_analysis)
411
+
412
+ if not job_analysis or not resume_analysis or not match_analysis:
413
+ st.error("Insufficient data returned from the API. Please try again.")
414
+ return
415
+
416
+ # Display Results
417
+ st.header("Analysis Results πŸ“Š")
418
+
419
+ # Match Overview
420
+ col1, col2, col3 = st.columns(3)
421
+ with col1:
422
+ st.metric(
423
+ "Overall Match 🎯",
424
+ f"{match_analysis.get('overall_match_percentage', '0%')}"
425
+ )
426
+ with col2:
427
+ st.metric(
428
+ "Skills Match 🧠",
429
+ f"{len(match_analysis.get('matching_skills', []))} skills"
430
+ )
431
+ with col3:
432
+ st.metric(
433
+ "Skills to Develop πŸ“ˆ",
434
+ f"{len(match_analysis.get('missing_skills', []))} skills"
435
+ )
436
+
437
+ # Detailed Analysis Tabs
438
+ tab1, tab2, tab3, tab4, tab5 = st.tabs([
439
+ "Skills Analysis πŸ“Š", "Experience Match πŸ—‚οΈ", "Recommendations πŸ’‘", "Cover Letter πŸ’Œ", "Updated Resume πŸ“"
440
+ ])
441
+
442
+ with tab1:
443
+ st.subheader("Matching Skills")
444
+ for skill in match_analysis.get('matching_skills', []):
445
+ st.success(f"βœ… {skill['skill_name']}")
446
+
447
+ st.subheader("Missing Skills")
448
+ for skill in match_analysis.get('missing_skills', []):
449
+ st.warning(f"⚠️ {skill['skill_name']}")
450
+ st.info(f"Suggestion: {skill['suggestion']}")
451
+
452
+ # Skills analysis graph
453
+ matching_skills_count = len(match_analysis.get('matching_skills', []))
454
+ missing_skills_count = len(match_analysis.get('missing_skills', []))
455
+ skills_data = pd.DataFrame({
456
+ 'Status': ['Matching', 'Missing'],
457
+ 'Count': [matching_skills_count, missing_skills_count]
458
+ })
459
+ fig = px.bar(skills_data, x='Status', y='Count', color='Status',
460
+ color_discrete_sequence=['#5cb85c', '#d9534f'], title='Skills Analysis')
461
+ fig.update_layout(xaxis_title='Status', yaxis_title='Count')
462
+ st.plotly_chart(fig)
463
+
464
+ with tab2:
465
+ st.write("### Experience Match Analysis πŸ—‚οΈ")
466
+ st.write(match_analysis.get('experience_match_analysis', ''))
467
+ st.write("### Education Match Analysis πŸŽ“")
468
+ st.write(match_analysis.get('education_match_analysis', ''))
469
+
470
+ with tab3:
471
+ st.write("### Key Recommendations πŸ”‘")
472
+ for rec in match_analysis.get('recommendations_for_improvement', []):
473
+ st.info(f"**{rec['recommendation']}**")
474
+ st.write(f"**Section:** {rec['section']}")
475
+ st.write(f"**Guidance:** {rec['guidance']}")
476
+
477
+ st.write("### ATS Optimization Suggestions πŸ€–")
478
+ for suggestion in match_analysis.get('ats_optimization_suggestions', []):
479
+ st.write("---")
480
+ st.warning(f"**Section to Modify:** {suggestion['section']}")
481
+ if suggestion.get('current_content'):
482
+ st.write(f"**Current Content:** {suggestion['current_content']}")
483
+ st.write(f"**Suggested Change:** {suggestion['suggested_change']}")
484
+ if suggestion.get('keywords_to_add'):
485
+ st.write(f"**Keywords to Add:** {', '.join(suggestion['keywords_to_add'])}")
486
+ if suggestion.get('formatting_suggestion'):
487
+ st.write(f"**Formatting Changes:** {suggestion['formatting_suggestion']}")
488
+ if suggestion.get('reason'):
489
+ st.info(f"**Reason for Change:** {suggestion['reason']}")
490
+
491
+ with tab4:
492
+ st.write("### Cover Letter Generator πŸ–ŠοΈ")
493
+ tone = st.selectbox("Select tone 🎭", ["Professional πŸ‘”", "Enthusiastic πŸ˜ƒ", "Confident 😎", "Friendly πŸ‘‹"])
494
+
495
+ if st.button("Generate Cover Letter ✍️"):
496
+ with st.spinner("✍️ Crafting your cover letter..."):
497
+ cover_letter = cover_letter_gen.generate_cover_letter(
498
+ job_analysis, resume_analysis, match_analysis, tone.lower().split()[0])
499
+ st.markdown("### Your Custom Cover Letter πŸ’Œ")
500
+ st.text_area("", cover_letter, height=400)
501
+ st.download_button(
502
+ "Download Cover Letter πŸ“₯",
503
+ cover_letter,
504
+ "cover_letter.txt",
505
+ "text/plain"
506
+ )
507
+
508
+ with tab5:
509
+ st.write("### Updated Resume πŸ“")
510
+ updated_resume = generate_updated_resume(resume_text, match_analysis)
511
+ # Provide a download button for the updated resume
512
+ st.download_button(
513
+ "Download Updated Resume πŸ“₯",
514
+ updated_resume,
515
+ "updated_resume.pdf",
516
+ mime="application/pdf"
517
+ )
518
 
519
 
520
  if __name__ == "__main__":
521
+ main()