PluginLiveInterns commited on
Commit
c85d1d3
·
1 Parent(s): 404d85c

Add application file

Browse files
Files changed (4) hide show
  1. .env +2 -0
  2. app.py +643 -0
  3. rag.py +99 -0
  4. requirements.txt +12 -0
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GEMINI_API_KEY=AIzaSyAOK9vRTSRQzd22B2gmbiuIePbZTDyaGYs
2
+ RAPIDAPI_KEY=a9712241damsh9d248dc7bd8afabp171beajsn5d30f6e126b7
app.py ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import http.client
3
+ import json
4
+ import os
5
+ import PyPDF2
6
+ import io
7
+ from google import genai
8
+ import requests
9
+ from dotenv import load_dotenv
10
+ import time
11
+
12
+ # Load environment variables from .env file
13
+ load_dotenv()
14
+
15
+ # Configure page
16
+ st.set_page_config(page_title="AI Job Finder", page_icon="💼", layout="wide")
17
+
18
+ # Styling
19
+ st.markdown("""
20
+ <style>
21
+ .main-header {
22
+ font-size: 2.5rem;
23
+ color: #4169E1;
24
+ }
25
+ .sub-header {
26
+ font-size: 1.5rem;
27
+ color: #6C757D;
28
+ }
29
+ .success-message {
30
+ background-color: #D4EDDA;
31
+ color: #155724;
32
+ padding: 10px;
33
+ border-radius: 5px;
34
+ margin-bottom: 20px;
35
+ }
36
+ .info-box {
37
+ background-color: #E7F3FE;
38
+ border-left: 6px solid #2196F3;
39
+ padding: 10px;
40
+ margin-bottom: 15px;
41
+ }
42
+ .search-options {
43
+ margin-top: 20px;
44
+ margin-bottom: 20px;
45
+ }
46
+ </style>
47
+ """, unsafe_allow_html=True)
48
+
49
+ # Header
50
+ st.markdown('<p class="main-header">AI-Powered Job Finder</p>', unsafe_allow_html=True)
51
+ st.markdown('<p class="sub-header">Upload your resume and find relevant jobs</p>', unsafe_allow_html=True)
52
+
53
+ # Initialize session state variables
54
+ if 'resume_text' not in st.session_state:
55
+ st.session_state.resume_text = ""
56
+ if 'resume_parsed' not in st.session_state:
57
+ st.session_state.resume_parsed = False
58
+ if 'parsed_data' not in st.session_state:
59
+ st.session_state.parsed_data = {}
60
+ if 'job_results' not in st.session_state:
61
+ st.session_state.job_results = []
62
+ if 'search_completed' not in st.session_state:
63
+ st.session_state.search_completed = False
64
+ if 'suggested_job_roles' not in st.session_state:
65
+ st.session_state.suggested_job_roles = []
66
+ if 'jobs_by_role' not in st.session_state:
67
+ st.session_state.jobs_by_role = {}
68
+
69
+ # Define the JSON schema for resume parsing
70
+ RESUME_SCHEMA = {
71
+ "schema": {
72
+ "basic_info": {
73
+ "name": "string",
74
+ "email": "string",
75
+ "phone": "string",
76
+ "location": "string"
77
+ },
78
+ "professional_summary": "string",
79
+ "skills": ["string"],
80
+ "technical_skills": ["string"],
81
+ "soft_skills": ["string"],
82
+ "experience": [{
83
+ "job_title": "string",
84
+ "company": "string",
85
+ "duration": "string",
86
+ "description": "string"
87
+ }],
88
+ "education": [{
89
+ "degree": "string",
90
+ "institution": "string",
91
+ "year": "string"
92
+ }],
93
+ "certifications": ["string"],
94
+ "years_of_experience": "number"
95
+ }
96
+ }
97
+
98
+ # Function to extract text from PDF
99
+ def extract_text_from_pdf(pdf_file):
100
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
101
+ text = ""
102
+ for page_num in range(len(pdf_reader.pages)):
103
+ text += pdf_reader.pages[page_num].extract_text()
104
+ return text
105
+
106
+ # Function to parse resume with Gemini
107
+ def parse_resume_with_gemini(resume_text):
108
+ try:
109
+ # Configure the Gemini API
110
+ client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
111
+
112
+ # Construct the prompt with schema
113
+ prompt = f"""
114
+ Parse the following resume text and extract information according to this exact JSON schema:
115
+
116
+ {json.dumps(RESUME_SCHEMA, indent=2)}
117
+
118
+ Resume text:
119
+ {resume_text}
120
+
121
+ Make sure to follow the schema exactly. If any information is not available, use empty strings or empty arrays as appropriate.
122
+ Return ONLY the JSON object with no additional text.
123
+ """
124
+
125
+ # Get the model
126
+
127
+ # Generate the response
128
+ response = client.models.generate_content(model="gemini-2.0-flash", contents=prompt)
129
+
130
+
131
+ # Parse the response to get JSON
132
+ try:
133
+ parsed_data = json.loads(response.text)
134
+ return parsed_data
135
+ except json.JSONDecodeError:
136
+ # Try to extract JSON from the text if not directly parseable
137
+ import re
138
+ json_match = re.search(r'```json\n(.*?)\n```', response.text, re.DOTALL)
139
+ if json_match:
140
+ return json.loads(json_match.group(1))
141
+ else:
142
+ st.error("Could not parse the response as JSON")
143
+ return RESUME_SCHEMA["schema"]
144
+ except Exception as e:
145
+ st.error(f"Error parsing resume: {str(e)}")
146
+ return RESUME_SCHEMA["schema"]
147
+
148
+ # Function to search for jobs
149
+ def search_jobs(query, location="", page=1):
150
+ try:
151
+ conn = http.client.HTTPSConnection("jsearch.p.rapidapi.com")
152
+
153
+ # Format the query string
154
+ search_query = query.replace(" ", "%20")
155
+ if location:
156
+ search_query += f"%20in%20{location.replace(' ', '%20')}"
157
+
158
+ headers = {
159
+ 'X-RapidAPI-Key': os.getenv('RAPIDAPI_KEY'),
160
+ 'X-RapidAPI-Host': "jsearch.p.rapidapi.com"
161
+ }
162
+
163
+ conn.request("GET", f"/search?query={search_query}&page={page}&num_pages=1", headers=headers)
164
+
165
+ res = conn.getresponse()
166
+ data = res.read()
167
+
168
+ return json.loads(data.decode("utf-8"))
169
+ except Exception as e:
170
+ st.error(f"Error searching for jobs: {str(e)}")
171
+ return {"data": []}
172
+
173
+ if 'filter_remote_only' not in st.session_state:
174
+ st.session_state.filter_remote_only = False
175
+ if 'filter_employment_types' not in st.session_state:
176
+ st.session_state.filter_employment_types = []
177
+ if 'filter_date_posted' not in st.session_state:
178
+ st.session_state.filter_date_posted = 0
179
+ if 'min_salary' not in st.session_state:
180
+ st.session_state.min_salary = 0
181
+ if 'max_salary' not in st.session_state:
182
+ st.session_state.max_salary = 1000000
183
+ if 'filter_company_types' not in st.session_state:
184
+ st.session_state.filter_company_types = []
185
+
186
+ # Function to apply filters to job results
187
+ def apply_filters(jobs):
188
+ filtered_jobs = []
189
+
190
+ for job in jobs:
191
+ # Check remote filter
192
+ if st.session_state.filter_remote_only and not job.get('job_is_remote', False):
193
+ continue
194
+
195
+ # Check employment type filter
196
+ if st.session_state.filter_employment_types and job.get('job_employment_type') not in st.session_state.filter_employment_types:
197
+ continue
198
+
199
+ # Check date posted filter (in days)
200
+ if st.session_state.filter_date_posted > 0:
201
+ current_time = int(time.time())
202
+ posted_time = job.get('job_posted_at_timestamp', 0)
203
+ days_ago = (current_time - posted_time) / (60 * 60 * 24)
204
+ if days_ago > st.session_state.filter_date_posted:
205
+ continue
206
+
207
+ # Check salary filter
208
+ if job.get('job_min_salary') is not None and job.get('job_min_salary') < st.session_state.min_salary:
209
+ continue
210
+
211
+ if job.get('job_max_salary') is not None and job.get('job_max_salary') > st.session_state.max_salary:
212
+ continue
213
+
214
+ # Check company type filter
215
+ if st.session_state.filter_company_types and job.get('employer_company_type') not in st.session_state.filter_company_types:
216
+ continue
217
+
218
+ # All filters passed, add job to filtered results
219
+ filtered_jobs.append(job)
220
+
221
+ return filtered_jobs
222
+
223
+ # Function to generate job role suggestions based on skills using Gemini
224
+ def suggest_job_roles(skills):
225
+ try:
226
+ # Configure the Gemini API
227
+ client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
228
+
229
+ # Construct the prompt for job role suggestions
230
+ prompt = f"""
231
+ Based on the following skills extracted from a resume, suggest 3-5 relevant job roles that this person could apply for.
232
+ Skills: {', '.join(skills)}
233
+
234
+ Return only a JSON array of strings with the job role titles. For example:
235
+ ["Software Developer", "Data Engineer", "DevOps Engineer"]
236
+
237
+ Make the job roles specific and relevant to the skills provided.
238
+ """
239
+
240
+ # Generate the response
241
+ response = client.models.generate_content(model="gemini-2.0-flash", contents=prompt)
242
+
243
+ # Parse the response to get job roles
244
+ try:
245
+ # Try to parse as direct JSON
246
+ job_roles = json.loads(response.text)
247
+ return job_roles
248
+ except json.JSONDecodeError:
249
+ # Try to extract JSON from text
250
+ import re
251
+ json_match = re.search(r'\[.*\]', response.text, re.DOTALL)
252
+ if json_match:
253
+ return json.loads(json_match.group(0))
254
+ else:
255
+ # Fallback
256
+ st.warning("Could not automatically generate job roles. Using default suggestions.")
257
+ return ["Software Developer", "Data Analyst", "Project Manager"]
258
+
259
+ except Exception as e:
260
+ st.error(f"Error suggesting job roles: {str(e)}")
261
+ return ["Software Developer", "Data Analyst", "Project Manager"]
262
+
263
+ # Function to search for jobs with multiple queries
264
+ def search_jobs_for_roles(job_roles, location="", page=1):
265
+ all_jobs = {}
266
+
267
+ for role in job_roles:
268
+ with st.spinner(f'Searching for {role} jobs...'):
269
+ result = search_jobs(role, location, page)
270
+ jobs = result.get('data', [])
271
+ all_jobs[role] = jobs
272
+
273
+ return all_jobs
274
+
275
+ # Resume Upload Section
276
+ st.subheader("Step 1: Upload Your Resume First")
277
+ uploaded_file = st.file_uploader("Upload your resume (PDF format)", type=['pdf'])
278
+
279
+ if uploaded_file is not None:
280
+ with st.spinner('Processing your resume...'):
281
+ # Extract text from the PDF
282
+ resume_text = extract_text_from_pdf(uploaded_file)
283
+ st.session_state.resume_text = resume_text
284
+
285
+ # Parse the resume
286
+ parsed_data = parse_resume_with_gemini(resume_text)
287
+ st.session_state.parsed_data = parsed_data
288
+ st.session_state.resume_parsed = True
289
+
290
+ # Extract all skills
291
+ tech_skills = parsed_data.get("technical_skills", [])
292
+ general_skills = parsed_data.get("skills", [])
293
+ soft_skills = parsed_data.get("soft_skills", [])
294
+ all_skills = list(set(tech_skills + general_skills + soft_skills))
295
+
296
+ # Generate job role suggestions
297
+ if all_skills:
298
+ st.session_state.suggested_job_roles = suggest_job_roles(all_skills)
299
+
300
+ if st.session_state.resume_parsed:
301
+ st.markdown("---")
302
+ st.subheader("Your Resume Information")
303
+ # Display the parsed information
304
+ with st.expander("Resume Parsed Information", expanded=True):
305
+ col1, col2 = st.columns(2)
306
+
307
+ with col1:
308
+ st.markdown("### Basic Information")
309
+ basic_info = parsed_data.get("basic_info", {})
310
+ st.write(f"**Name:** {basic_info.get('name', 'Not found')}")
311
+ st.write(f"**Email:** {basic_info.get('email', 'Not found')}")
312
+ st.write(f"**Phone:** {basic_info.get('phone', 'Not found')}")
313
+ st.write(f"**Location:** {basic_info.get('location', 'Not found')}")
314
+
315
+ st.markdown("### Experience")
316
+ for exp in parsed_data.get("experience", []):
317
+ st.markdown(f"**{exp.get('job_title', 'Role')} at {exp.get('company', 'Company')}**")
318
+ st.write(f"*{exp.get('duration', 'Duration not specified')}*")
319
+ st.write(exp.get('description', 'No description available'))
320
+ st.write("---")
321
+
322
+ with col2:
323
+ st.markdown("### Skills")
324
+
325
+ # Technical skills
326
+ st.write("**Technical Skills:**")
327
+ tech_skills = parsed_data.get("technical_skills", [])
328
+ if tech_skills:
329
+ st.write(", ".join(tech_skills))
330
+ else:
331
+ st.write("No technical skills found")
332
+
333
+ # Soft skills
334
+ st.write("**Soft Skills:**")
335
+ soft_skills = parsed_data.get("soft_skills", [])
336
+ if soft_skills:
337
+ st.write(", ".join(soft_skills))
338
+ else:
339
+ st.write("No soft skills found")
340
+
341
+ # General skills
342
+ st.write("**General Skills:**")
343
+ skills = parsed_data.get("skills", [])
344
+ if skills:
345
+ st.write(", ".join(skills))
346
+ else:
347
+ st.write("No general skills found")
348
+
349
+ st.markdown("### Education")
350
+ for edu in parsed_data.get("education", []):
351
+ st.write(f"**{edu.get('degree', 'Degree')}** - {edu.get('institution', 'Institution')}")
352
+ st.write(f"*{edu.get('year', 'Year not specified')}*")
353
+
354
+ st.write(f"**Years of Experience:** {parsed_data.get('years_of_experience', 'Not specified')}")
355
+ st.markdown("---")
356
+ st.subheader("Step 2: Job Search")
357
+
358
+ # Location input only (job roles are now automated)
359
+ location = st.text_input("Enter your preferred location (e.g., 'New York', 'Remote')")
360
+
361
+ # Display suggested job roles if available
362
+ if st.session_state.resume_parsed and st.session_state.suggested_job_roles:
363
+ st.markdown("### Suggested Job Roles Based on Your Skills")
364
+
365
+ # Add custom roles to session state if not already there
366
+ if 'custom_job_roles' not in st.session_state:
367
+ st.session_state.custom_job_roles = []
368
+
369
+ # Combine suggested and custom roles
370
+ all_job_roles = list(st.session_state.suggested_job_roles) + list(st.session_state.custom_job_roles)
371
+
372
+ # Create a row with text input and add button
373
+ col1, col2 = st.columns([3, 1])
374
+ with col1:
375
+ custom_role = st.text_input("Add your own job role", key="custom_role_input")
376
+ with col2:
377
+ if st.button("Add Role"):
378
+ if custom_role and custom_role not in all_job_roles:
379
+ st.session_state.custom_job_roles.append(custom_role)
380
+ st.success(f"Added: {custom_role}")
381
+ # Rerun to update the interface
382
+ st.rerun()
383
+
384
+ # Display job roles as selectable options (both suggested and custom)
385
+ selected_roles = st.multiselect(
386
+ "Select job roles to search for",
387
+ options=all_job_roles,
388
+ default=st.session_state.suggested_job_roles
389
+ )
390
+
391
+ # Display job roles as selectable options
392
+ # selected_roles = st.multiselect(
393
+ # "Select job roles to search for",
394
+ # options=st.session_state.suggested_job_roles,
395
+ # default=st.session_state.suggested_job_roles
396
+ # )
397
+
398
+ # Add filter options to sidebar
399
+ st.sidebar.markdown("### Filter Options")
400
+
401
+ # Remote work filter
402
+ st.sidebar.checkbox("Remote Only", key="filter_remote_only")
403
+
404
+ # Employment type filter
405
+ employment_types = ["FULLTIME", "PARTTIME", "CONTRACTOR", "INTERN"]
406
+ st.sidebar.multiselect(
407
+ "Employment Type",
408
+ employment_types,
409
+ default=None,
410
+ key="filter_employment_types"
411
+ )
412
+
413
+ # Date posted filter
414
+ date_options = {
415
+ "Any time": 0,
416
+ "Past 24 hours": 1,
417
+ "Past week": 7,
418
+ "Past month": 30
419
+ }
420
+ selected_date = st.sidebar.selectbox(
421
+ "Date Posted",
422
+ options=list(date_options.keys()),
423
+ index=0
424
+ )
425
+ st.session_state.filter_date_posted = date_options[selected_date]
426
+
427
+ # Salary range filter (only if salary data is available)
428
+ st.sidebar.markdown("### Salary Range")
429
+ col1, col2 = st.sidebar.columns(2)
430
+ with col1:
431
+ st.number_input("Min ($)", value=0, step=10000, key="min_salary")
432
+ with col2:
433
+ st.number_input("Max ($)", value=1000000, step=10000, key="max_salary")
434
+
435
+ # Company type filter
436
+ company_types = ["Public", "Private", "Nonprofit", "Government", "Startup", "Other"]
437
+ st.sidebar.multiselect(
438
+ "Company Type",
439
+ company_types,
440
+ default=None,
441
+ key="filter_company_types"
442
+ )
443
+
444
+ # Search button
445
+ if st.button("Search Jobs"):
446
+ if selected_roles:
447
+ with st.spinner('Searching for jobs across selected roles...'):
448
+ # Search for jobs for each selected role
449
+ jobs_by_role = search_jobs_for_roles(selected_roles, location)
450
+
451
+ # Store the results in session state
452
+ st.session_state.jobs_by_role = jobs_by_role
453
+ st.session_state.search_completed = True
454
+ else:
455
+ st.warning("Please select at least one job role")
456
+ else:
457
+ # If no resume uploaded or no job roles suggested
458
+ st.info("Upload your resume first to get AI-suggested job roles based on your skills")
459
+
460
+ # Manual job search fallback
461
+ search_query = st.text_input("Or enter your job search query manually (e.g., 'Python Developer')")
462
+
463
+ if st.button("Search Jobs"):
464
+ if search_query:
465
+ with st.spinner('Searching for jobs...'):
466
+ # Search for jobs
467
+ job_results = search_jobs(search_query, location)
468
+
469
+ # Store the results in session state
470
+ st.session_state.job_results = job_results.get('data', [])
471
+ st.session_state.jobs_by_role = {search_query: job_results.get('data', [])}
472
+ st.session_state.search_completed = True
473
+ else:
474
+ st.warning("Please enter a search query or upload your resume for job suggestions")
475
+
476
+ # Display Results
477
+ if st.session_state.search_completed:
478
+ st.markdown("---")
479
+ st.subheader("Job Search Results")
480
+
481
+ if st.session_state.jobs_by_role:
482
+ total_jobs_found = sum(len(jobs) for jobs in st.session_state.jobs_by_role.values())
483
+ st.success(f"Found a total of {total_jobs_found} jobs matching your criteria")
484
+
485
+ # Display jobs grouped by role
486
+ for role, jobs in st.session_state.jobs_by_role.items():
487
+ if jobs:
488
+ # Apply filters to this role's jobs
489
+ filtered_jobs = apply_filters(jobs)
490
+
491
+ if filtered_jobs:
492
+ with st.expander(f"📌 {role} Jobs ({len(filtered_jobs)})", expanded=False):
493
+ st.markdown(f"### {role} Positions")
494
+
495
+ # Calculate skill match percentages if resume is uploaded
496
+ if st.session_state.resume_parsed:
497
+ # Extract all skills from resume
498
+ tech_skills = set(st.session_state.parsed_data.get("technical_skills", []))
499
+ general_skills = set(st.session_state.parsed_data.get("skills", []))
500
+ soft_skills = set(st.session_state.parsed_data.get("soft_skills", []))
501
+ all_skills = tech_skills.union(general_skills).union(soft_skills)
502
+
503
+ # Add match score to each job
504
+ for job in filtered_jobs:
505
+ if job.get('job_description'):
506
+ desc = job.get('job_description', '').lower()
507
+ matched_skills = [skill for skill in all_skills if skill.lower() in desc]
508
+ match_percentage = int((len(matched_skills) / max(1, len(all_skills))) * 100)
509
+ job['match_percentage'] = match_percentage
510
+ job['matched_skills'] = matched_skills
511
+ else:
512
+ job['match_percentage'] = 0
513
+ job['matched_skills'] = []
514
+
515
+ # Sort by match percentage
516
+ filtered_jobs = sorted(filtered_jobs, key=lambda x: x.get('match_percentage', 0), reverse=True)
517
+
518
+ # Display each job in this role category
519
+ for job_idx, job in enumerate(filtered_jobs):
520
+ # Customize job title based on match percentage if resume uploaded
521
+ if st.session_state.resume_parsed and 'match_percentage' in job:
522
+ job_title = f"{job_idx+1}. {job.get('job_title', 'Job Title Not Available')} - {job.get('employer_name', 'Company Not Available')} "
523
+ job_title += f"[Match: {job.get('match_percentage')}%]"
524
+ else:
525
+ job_title = f"{job_idx+1}. {job.get('job_title', 'Job Title Not Available')} - {job.get('employer_name', 'Company Not Available')}"
526
+
527
+ # Use a container instead of an expander to avoid nesting
528
+ job_container = st.container()
529
+
530
+ # Add a visual separator between jobs
531
+ st.markdown("---")
532
+
533
+ # Display job title with formatted styling
534
+ job_container.markdown(f"#### {job_title}")
535
+
536
+ # Create columns for job details
537
+ cols = job_container.columns([2, 1])
538
+
539
+ with cols[0]:
540
+ # Job details
541
+ st.write(f"**Company:** {job.get('employer_name', 'Not Available')}")
542
+ st.write(f"**Location:** {job.get('job_city', 'Not Available')}, {job.get('job_country', 'Not Available')}")
543
+ st.write(f"**Employment Type:** {job.get('job_employment_type', 'Not Available')}")
544
+
545
+ # Remote information
546
+ st.write(f"**Remote:** {'Yes' if job.get('job_is_remote') else 'No'}")
547
+
548
+ # Date posted and expiration
549
+ if job.get('job_posted_at_datetime_utc'):
550
+ st.write(f"**Posted:** {job.get('job_posted_at_datetime_utc', 'Not Available')}")
551
+
552
+ # Salary information
553
+ if job.get('job_min_salary') and job.get('job_max_salary'):
554
+ st.write(f"**Salary Range:** ${job.get('job_min_salary', 'Not Available')} - ${job.get('job_max_salary', 'Not Available')} {job.get('job_salary_currency', 'USD')}")
555
+
556
+ with cols[1]:
557
+ # Enhanced skills match section
558
+ if st.session_state.resume_parsed:
559
+ match_percentage = job.get('match_percentage', 0)
560
+ matched_skills = job.get('matched_skills', [])
561
+
562
+ # Create a visual progress bar for match percentage
563
+ st.markdown("### Skills Match")
564
+
565
+ # Color coding based on match percentage
566
+ if match_percentage > 70:
567
+ bar_color = "green"
568
+ elif match_percentage > 40:
569
+ bar_color = "orange"
570
+ else:
571
+ bar_color = "red"
572
+
573
+ # Display progress bar
574
+ st.progress(match_percentage / 100)
575
+ st.markdown(f"<h4 style='color:{bar_color};margin-top:0'>{match_percentage}% Match</h4>", unsafe_allow_html=True)
576
+
577
+ if matched_skills:
578
+ st.markdown("**Matching Skills:**")
579
+ skill_cols = st.columns(2)
580
+ for skill_idx, skill in enumerate(matched_skills[:10]):
581
+ col_idx = skill_idx % 2
582
+ with skill_cols[col_idx]:
583
+ st.markdown(f"✅ {skill}")
584
+
585
+ if len(matched_skills) > 10:
586
+ st.markdown(f"*...and {len(matched_skills)-10} more*")
587
+ else:
588
+ st.write("⚠️ No direct skill matches found")
589
+
590
+ # Description
591
+ job_container.markdown("**Job Description:**")
592
+ full_desc = job.get('job_description', 'No description available')
593
+
594
+ if len(full_desc) > 1000:
595
+ job_container.markdown(full_desc[:1000] + "...")
596
+ if job_container.button(f"Show Full Description for Job {job_idx+1}", key=f"show_desc_{role}_{job_idx}"):
597
+ job_container.markdown(full_desc)
598
+ else:
599
+ job_container.markdown(full_desc)
600
+
601
+ # Display ALL application links
602
+ job_container.markdown("**Apply Links:**")
603
+ apply_options = job.get('apply_options', [])
604
+ if apply_options:
605
+ for option in apply_options:
606
+ job_container.markdown(f"[Apply on {option.get('publisher', 'Job Board')}]({option.get('apply_link')})")
607
+ elif job.get('job_apply_link'):
608
+ job_container.markdown(f"[Apply for this job]({job.get('job_apply_link')})")
609
+ else:
610
+ st.info(f"No {role} jobs match your filters. Try adjusting your filter criteria.")
611
+ else:
612
+ st.info(f"No {role} jobs found matching your search criteria.")
613
+ else:
614
+ st.info("No jobs found matching your search criteria. Try adjusting your search terms or location.")
615
+
616
+
617
+ st.markdown("---")
618
+ st.markdown("### How to use this app")
619
+ st.markdown("""
620
+ 1. Upload your resume in PDF format to extract your skills and experience
621
+ 2. Enter your job search query and preferred location
622
+ 3. Review job listings and apply directly to positions you're interested in
623
+ """)
624
+
625
+
626
+ # Display app statistics
627
+ st.sidebar.markdown("### App Statistics")
628
+ if st.session_state.resume_parsed:
629
+ st.sidebar.success("✅ Resume Parsed")
630
+ skill_count = len(st.session_state.parsed_data.get("skills", [])) + len(st.session_state.parsed_data.get("technical_skills", []))
631
+ st.sidebar.metric("Skills Detected", skill_count)
632
+
633
+ if st.session_state.suggested_job_roles:
634
+ st.sidebar.metric("Job Roles Suggested", len(st.session_state.suggested_job_roles))
635
+ else:
636
+ st.sidebar.warning("❌ No Resume Uploaded")
637
+
638
+ if st.session_state.search_completed:
639
+ st.sidebar.success("✅ Job Search Completed")
640
+ total_jobs = sum(len(jobs) for jobs in st.session_state.jobs_by_role.values()) if st.session_state.jobs_by_role else len(st.session_state.job_results)
641
+ st.sidebar.metric("Jobs Found", total_jobs)
642
+ else:
643
+ st.sidebar.warning("❌ No Search Performed")
rag.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag.py
2
+ from sentence_transformers import SentenceTransformer
3
+ import faiss
4
+ import numpy as np
5
+ import google as genai
6
+ import os
7
+
8
+ class SimpleRAG:
9
+ def __init__(self, api_key):
10
+ # Initialize the embedding model and generative AI
11
+ self.embedder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
12
+ genai.configure(api_key=api_key)
13
+ self.model = genai.GenerativeModel("gemini-1.5-flash")
14
+ self.index = None
15
+ self.chunks = []
16
+ self.is_initialized = False
17
+ self.processing_status = None
18
+
19
+ def chunk_text(self, text, chunk_size=700):
20
+ """Split text into smaller chunks."""
21
+ words = text.split()
22
+ return [' '.join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
23
+
24
+ def process_search_data(self, search_data):
25
+ """
26
+ Process search result data and index it.
27
+ 'search_data' should be a list of job posting dictionaries.
28
+ For each job posting, we combine key fields (e.g., job title and description) and then chunk the text.
29
+ """
30
+ try:
31
+ self.processing_status = "Processing search data..."
32
+ combined_text = ""
33
+ for job in search_data:
34
+ # Combine job title and job description (you can add more fields if needed)
35
+ job_title = job.get('job_title', '')
36
+ job_description = job.get('job_description', '')
37
+ combined_text += f"Job Title: {job_title}. Description: {job_description}. "
38
+
39
+ if not combined_text.strip():
40
+ raise Exception("No text found in search results.")
41
+
42
+ # Chunk the combined text
43
+ self.chunks = self.chunk_text(combined_text)
44
+ if not self.chunks:
45
+ raise Exception("No content chunks were generated from search data.")
46
+
47
+ # Generate embeddings and create the FAISS index
48
+ embeddings = self.embedder.encode(self.chunks)
49
+ vector_dimension = embeddings.shape[1]
50
+ self.index = faiss.IndexFlatL2(vector_dimension)
51
+ self.index.add(np.array(embeddings).astype('float32'))
52
+
53
+ self.is_initialized = True
54
+ self.processing_status = f"RAG system initialized with {len(self.chunks)} chunks."
55
+ return {"status": "success", "message": self.processing_status}
56
+ except Exception as e:
57
+ self.processing_status = f"Error: {str(e)}"
58
+ self.is_initialized = False
59
+ return {"status": "error", "message": str(e)}
60
+
61
+ def get_status(self):
62
+ """Return the current processing status."""
63
+ return {
64
+ "is_initialized": self.is_initialized,
65
+ "status": self.processing_status
66
+ }
67
+
68
+ def get_relevant_chunks(self, query, k=3):
69
+ """Retrieve the top-k most relevant text chunks for a given query."""
70
+ query_vector = self.embedder.encode([query])
71
+ distances, chunk_indices = self.index.search(query_vector.astype('float32'), k)
72
+ return [self.chunks[i] for i in chunk_indices[0]]
73
+
74
+ def query(self, question):
75
+ """Query the RAG system with a user question."""
76
+ if not self.is_initialized:
77
+ raise Exception("RAG system not initialized. Please process search data first.")
78
+ try:
79
+ context = self.get_relevant_chunks(question)
80
+ prompt = f"""
81
+ Based on the following context, provide a clear and concise answer.
82
+ If the context doesn't contain enough relevant information, say "I don't have enough information to answer that question."
83
+
84
+ Context:
85
+ {' '.join(context)}
86
+
87
+ Question: {question}
88
+ """
89
+ response = self.model.generate_content(prompt)
90
+ return {
91
+ "status": "success",
92
+ "answer": response.text.strip(),
93
+ "context": context
94
+ }
95
+ except Exception as e:
96
+ return {
97
+ "status": "error",
98
+ "message": str(e)
99
+ }
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ python-dotenv
3
+ PyPDF2
4
+ requests
5
+ sentence-transformers
6
+ faiss-cpu
7
+ numpy
8
+ google-genai
9
+ certifi
10
+ charset-normalizer
11
+ idna
12
+ urllib3