Spaces:
Paused
Paused
Commit
·
2445440
1
Parent(s):
329b59a
resume parser removed
Browse files- README +0 -0
- app.py +46 -43
- backend/models/resume_parser/resume_to_features.py +0 -251
- backend/routes/auth.py +27 -10
- backend/services/interview_engine.py +37 -7
- backend/templates/apply.html +1 -1
- backend/utils/luna_phase1.py +0 -0
- requirements.txt +3 -12
README
DELETED
|
File without changes
|
app.py
CHANGED
|
@@ -26,10 +26,8 @@ sys.path.append(current_dir)
|
|
| 26 |
# Import and initialize DB
|
| 27 |
from backend.models.database import db, Job, Application, init_db
|
| 28 |
from backend.models.user import User
|
| 29 |
-
from backend.routes.auth import auth_bp
|
| 30 |
from backend.routes.interview_api import interview_api
|
| 31 |
-
from backend.models.resume_parser.resume_to_features import extract_resume_features
|
| 32 |
-
|
| 33 |
# Initialize Flask app
|
| 34 |
app = Flask(
|
| 35 |
__name__,
|
|
@@ -88,31 +86,6 @@ def load_user(user_id):
|
|
| 88 |
app.register_blueprint(auth_bp)
|
| 89 |
app.register_blueprint(interview_api, url_prefix="/api")
|
| 90 |
|
| 91 |
-
def handle_resume_upload(file):
|
| 92 |
-
"""Save uploaded file temporarily, extract features, then clean up."""
|
| 93 |
-
if not file or file.filename == '':
|
| 94 |
-
return None, "No file uploaded", None
|
| 95 |
-
|
| 96 |
-
try:
|
| 97 |
-
filename = secure_filename(file.filename)
|
| 98 |
-
temp_dir = '/tmp/temp' # Use /tmp for temporary files
|
| 99 |
-
os.makedirs(temp_dir, exist_ok=True)
|
| 100 |
-
filepath = os.path.join(temp_dir, filename)
|
| 101 |
-
|
| 102 |
-
file.save(filepath)
|
| 103 |
-
features = extract_resume_features(filepath)
|
| 104 |
-
|
| 105 |
-
# Clean up
|
| 106 |
-
try:
|
| 107 |
-
os.remove(filepath)
|
| 108 |
-
except:
|
| 109 |
-
pass
|
| 110 |
-
|
| 111 |
-
return features, None, filename
|
| 112 |
-
except Exception as e:
|
| 113 |
-
print(f"Error in handle_resume_upload: {e}")
|
| 114 |
-
return None, str(e), None
|
| 115 |
-
|
| 116 |
# Routes (keep your existing routes)
|
| 117 |
@app.route('/')
|
| 118 |
def index():
|
|
@@ -132,29 +105,43 @@ def job_detail(job_id):
|
|
| 132 |
@login_required
|
| 133 |
def apply(job_id):
|
| 134 |
job = Job.query.get_or_404(job_id)
|
| 135 |
-
|
| 136 |
if request.method == 'POST':
|
|
|
|
|
|
|
| 137 |
file = request.files.get('resume')
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
return render_template('apply.html', job=job)
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
application = Application(
|
| 145 |
job_id=job_id,
|
| 146 |
user_id=current_user.id,
|
| 147 |
name=current_user.username,
|
| 148 |
email=current_user.email,
|
|
|
|
| 149 |
extracted_features=json.dumps(features)
|
| 150 |
)
|
| 151 |
-
|
| 152 |
db.session.add(application)
|
| 153 |
db.session.commit()
|
| 154 |
-
|
| 155 |
flash('Your application has been submitted successfully!', 'success')
|
| 156 |
return redirect(url_for('jobs'))
|
| 157 |
-
|
| 158 |
return render_template('apply.html', job=job)
|
| 159 |
|
| 160 |
@app.route('/my_applications')
|
|
@@ -168,14 +155,30 @@ def my_applications():
|
|
| 168 |
@app.route('/parse_resume', methods=['POST'])
|
| 169 |
def parse_resume():
|
| 170 |
file = request.files.get('resume')
|
| 171 |
-
features, error,
|
| 172 |
-
|
|
|
|
|
|
|
| 173 |
if error:
|
| 174 |
-
return {"error": "Error
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
if not features:
|
| 177 |
-
return {
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
response = {
|
| 180 |
"name": features.get('name', ''),
|
| 181 |
"email": features.get('email', ''),
|
|
|
|
| 26 |
# Import and initialize DB
|
| 27 |
from backend.models.database import db, Job, Application, init_db
|
| 28 |
from backend.models.user import User
|
| 29 |
+
from backend.routes.auth import auth_bp, handle_resume_upload
|
| 30 |
from backend.routes.interview_api import interview_api
|
|
|
|
|
|
|
| 31 |
# Initialize Flask app
|
| 32 |
app = Flask(
|
| 33 |
__name__,
|
|
|
|
| 86 |
app.register_blueprint(auth_bp)
|
| 87 |
app.register_blueprint(interview_api, url_prefix="/api")
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
# Routes (keep your existing routes)
|
| 90 |
@app.route('/')
|
| 91 |
def index():
|
|
|
|
| 105 |
@login_required
|
| 106 |
def apply(job_id):
|
| 107 |
job = Job.query.get_or_404(job_id)
|
|
|
|
| 108 |
if request.method == 'POST':
|
| 109 |
+
# Retrieve the uploaded resume file from the request. The ``name``
|
| 110 |
+
# attribute in the HTML form is ``resume``.
|
| 111 |
file = request.files.get('resume')
|
| 112 |
+
# Use our safe upload helper to store the resume and obtain an empty
|
| 113 |
+
# features dictionary. ``filepath`` contains the location where the
|
| 114 |
+
# file was saved, allowing us to persist a reference in the database.
|
| 115 |
+
features, error, filepath = handle_resume_upload(file)
|
| 116 |
+
|
| 117 |
+
# If there was an error saving the resume, notify the user. We no
|
| 118 |
+
# longer attempt to parse the resume contents, so an empty
|
| 119 |
+
# features dictionary is considered valid.
|
| 120 |
+
if error:
|
| 121 |
+
flash("Resume upload failed. Please try again.", "danger")
|
| 122 |
return render_template('apply.html', job=job)
|
| 123 |
+
|
| 124 |
+
# Ensure features is a dictionary for JSON serialization. An empty
|
| 125 |
+
# dictionary results in a non-empty JSON string ("{}"), which is
|
| 126 |
+
# truthy and enables the interview feature on the applications page.
|
| 127 |
+
if not features:
|
| 128 |
+
features = {}
|
| 129 |
+
|
| 130 |
application = Application(
|
| 131 |
job_id=job_id,
|
| 132 |
user_id=current_user.id,
|
| 133 |
name=current_user.username,
|
| 134 |
email=current_user.email,
|
| 135 |
+
resume_path=filepath,
|
| 136 |
extracted_features=json.dumps(features)
|
| 137 |
)
|
| 138 |
+
|
| 139 |
db.session.add(application)
|
| 140 |
db.session.commit()
|
| 141 |
+
|
| 142 |
flash('Your application has been submitted successfully!', 'success')
|
| 143 |
return redirect(url_for('jobs'))
|
| 144 |
+
|
| 145 |
return render_template('apply.html', job=job)
|
| 146 |
|
| 147 |
@app.route('/my_applications')
|
|
|
|
| 155 |
@app.route('/parse_resume', methods=['POST'])
|
| 156 |
def parse_resume():
|
| 157 |
file = request.files.get('resume')
|
| 158 |
+
features, error, filepath = handle_resume_upload(file)
|
| 159 |
+
|
| 160 |
+
# If the upload failed, return an error. Parsing is no longer
|
| 161 |
+
# supported, so we do not attempt to inspect the resume contents.
|
| 162 |
if error:
|
| 163 |
+
return {"error": "Error processing resume. Please try again."}, 400
|
| 164 |
+
|
| 165 |
+
# If no features were extracted (the normal case now), respond with
|
| 166 |
+
# empty fields rather than an error. This preserves the API
|
| 167 |
+
# contract expected by any front‑end code that might call this
|
| 168 |
+
# endpoint.
|
| 169 |
if not features:
|
| 170 |
+
return {
|
| 171 |
+
"name": "",
|
| 172 |
+
"email": "",
|
| 173 |
+
"mobile_number": "",
|
| 174 |
+
"skills": [],
|
| 175 |
+
"experience": [],
|
| 176 |
+
"education": [],
|
| 177 |
+
"summary": ""
|
| 178 |
+
}, 200
|
| 179 |
+
|
| 180 |
+
# Should features contain values (unlikely in the new implementation),
|
| 181 |
+
# pass them through to the client.
|
| 182 |
response = {
|
| 183 |
"name": features.get('name', ''),
|
| 184 |
"email": features.get('email', ''),
|
backend/models/resume_parser/resume_to_features.py
DELETED
|
@@ -1,251 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import re
|
| 3 |
-
import json
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
import PyPDF2
|
| 6 |
-
from docx import Document
|
| 7 |
-
import textract
|
| 8 |
-
|
| 9 |
-
class SimpleResumeParser:
|
| 10 |
-
def __init__(self):
|
| 11 |
-
# Common skills keywords
|
| 12 |
-
self.skills_keywords = [
|
| 13 |
-
'python', 'javascript', 'java', 'c++', 'c#', 'php', 'ruby', 'go', 'rust',
|
| 14 |
-
'html', 'css', 'react', 'angular', 'vue', 'node.js', 'express', 'django',
|
| 15 |
-
'flask', 'spring', 'laravel', 'rails', 'asp.net', 'jquery', 'bootstrap',
|
| 16 |
-
'sql', 'mysql', 'postgresql', 'mongodb', 'redis', 'elasticsearch',
|
| 17 |
-
'aws', 'azure', 'gcp', 'docker', 'kubernetes', 'jenkins', 'git', 'github',
|
| 18 |
-
'machine learning', 'deep learning', 'tensorflow', 'pytorch', 'scikit-learn',
|
| 19 |
-
'data analysis', 'pandas', 'numpy', 'matplotlib', 'tableau', 'power bi',
|
| 20 |
-
'agile', 'scrum', 'devops', 'ci/cd', 'microservices', 'api', 'rest', 'graphql'
|
| 21 |
-
]
|
| 22 |
-
|
| 23 |
-
# Education keywords
|
| 24 |
-
self.education_keywords = [
|
| 25 |
-
'bachelor', 'master', 'phd', 'degree', 'university', 'college', 'institute',
|
| 26 |
-
'computer science', 'engineering', 'mathematics', 'physics', 'chemistry',
|
| 27 |
-
'business', 'mba', 'certification', 'diploma'
|
| 28 |
-
]
|
| 29 |
-
|
| 30 |
-
# Experience keywords
|
| 31 |
-
self.experience_keywords = [
|
| 32 |
-
'experience', 'worked', 'developed', 'managed', 'led', 'created', 'built',
|
| 33 |
-
'designed', 'implemented', 'maintained', 'optimized', 'improved', 'years'
|
| 34 |
-
]
|
| 35 |
-
|
| 36 |
-
def extract_text_from_pdf(self, file_path):
|
| 37 |
-
"""Extract text from PDF file"""
|
| 38 |
-
try:
|
| 39 |
-
with open(file_path, 'rb') as file:
|
| 40 |
-
reader = PyPDF2.PdfReader(file)
|
| 41 |
-
text = ""
|
| 42 |
-
for page in reader.pages:
|
| 43 |
-
text += page.extract_text() + "\n"
|
| 44 |
-
return text
|
| 45 |
-
except Exception as e:
|
| 46 |
-
print(f"Error reading PDF: {e}")
|
| 47 |
-
return ""
|
| 48 |
-
|
| 49 |
-
def extract_text_from_docx(self, file_path):
|
| 50 |
-
"""Extract text from DOCX file"""
|
| 51 |
-
try:
|
| 52 |
-
doc = Document(file_path)
|
| 53 |
-
text = ""
|
| 54 |
-
for paragraph in doc.paragraphs:
|
| 55 |
-
text += paragraph.text + "\n"
|
| 56 |
-
return text
|
| 57 |
-
except Exception as e:
|
| 58 |
-
print(f"Error reading DOCX: {e}")
|
| 59 |
-
return ""
|
| 60 |
-
|
| 61 |
-
def extract_text_from_doc(self, file_path):
|
| 62 |
-
"""Extract text from DOC file using textract"""
|
| 63 |
-
try:
|
| 64 |
-
text = textract.process(file_path).decode('utf-8')
|
| 65 |
-
return text
|
| 66 |
-
except Exception as e:
|
| 67 |
-
print(f"Error reading DOC: {e}")
|
| 68 |
-
return ""
|
| 69 |
-
|
| 70 |
-
def extract_text(self, file_path):
|
| 71 |
-
"""Extract text based on file extension"""
|
| 72 |
-
file_extension = Path(file_path).suffix.lower()
|
| 73 |
-
|
| 74 |
-
if file_extension == '.pdf':
|
| 75 |
-
return self.extract_text_from_pdf(file_path)
|
| 76 |
-
elif file_extension == '.docx':
|
| 77 |
-
return self.extract_text_from_docx(file_path)
|
| 78 |
-
elif file_extension == '.doc':
|
| 79 |
-
return self.extract_text_from_doc(file_path)
|
| 80 |
-
else:
|
| 81 |
-
return ""
|
| 82 |
-
|
| 83 |
-
def extract_email(self, text):
|
| 84 |
-
"""Extract email addresses from text"""
|
| 85 |
-
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
|
| 86 |
-
emails = re.findall(email_pattern, text)
|
| 87 |
-
return emails[0] if emails else ""
|
| 88 |
-
|
| 89 |
-
def extract_phone(self, text):
|
| 90 |
-
"""Extract phone numbers from text"""
|
| 91 |
-
phone_patterns = [
|
| 92 |
-
r'\+?1?[-.\s]?$$?([0-9]{3})$$?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})',
|
| 93 |
-
r'\+?([0-9]{1,3})[-.\s]?([0-9]{3,4})[-.\s]?([0-9]{3,4})[-.\s]?([0-9]{3,4})',
|
| 94 |
-
r'(\d{3}[-.\s]?\d{3}[-.\s]?\d{4})',
|
| 95 |
-
r'($$\d{3}$$\s?\d{3}[-.\s]?\d{4})'
|
| 96 |
-
]
|
| 97 |
-
|
| 98 |
-
for pattern in phone_patterns:
|
| 99 |
-
matches = re.findall(pattern, text)
|
| 100 |
-
if matches:
|
| 101 |
-
if isinstance(matches[0], tuple):
|
| 102 |
-
return ''.join(matches[0])
|
| 103 |
-
return matches[0]
|
| 104 |
-
return ""
|
| 105 |
-
|
| 106 |
-
def extract_name(self, text):
|
| 107 |
-
"""Extract name from text (simple heuristic)"""
|
| 108 |
-
lines = text.split('\n')
|
| 109 |
-
for line in lines[:5]: # Check first 5 lines
|
| 110 |
-
line = line.strip()
|
| 111 |
-
if len(line.split()) == 2 and line.replace(' ', '').isalpha():
|
| 112 |
-
# Simple check: two words, all alphabetic
|
| 113 |
-
if not any(keyword in line.lower() for keyword in ['resume', 'cv', 'curriculum']):
|
| 114 |
-
return line.title()
|
| 115 |
-
return ""
|
| 116 |
-
|
| 117 |
-
def extract_skills(self, text):
|
| 118 |
-
"""Extract skills from text"""
|
| 119 |
-
text_lower = text.lower()
|
| 120 |
-
found_skills = []
|
| 121 |
-
|
| 122 |
-
for skill in self.skills_keywords:
|
| 123 |
-
if skill.lower() in text_lower:
|
| 124 |
-
found_skills.append(skill.title())
|
| 125 |
-
|
| 126 |
-
# Remove duplicates and return
|
| 127 |
-
return list(set(found_skills))
|
| 128 |
-
|
| 129 |
-
def extract_education(self, text):
|
| 130 |
-
"""Extract education information"""
|
| 131 |
-
text_lower = text.lower()
|
| 132 |
-
education = []
|
| 133 |
-
|
| 134 |
-
# Look for education section
|
| 135 |
-
education_section = ""
|
| 136 |
-
lines = text.split('\n')
|
| 137 |
-
in_education_section = False
|
| 138 |
-
|
| 139 |
-
for line in lines:
|
| 140 |
-
line_lower = line.lower()
|
| 141 |
-
if any(keyword in line_lower for keyword in ['education', 'academic', 'qualification']):
|
| 142 |
-
in_education_section = True
|
| 143 |
-
continue
|
| 144 |
-
elif in_education_section and any(keyword in line_lower for keyword in ['experience', 'work', 'employment', 'project']):
|
| 145 |
-
break
|
| 146 |
-
elif in_education_section:
|
| 147 |
-
education_section += line + " "
|
| 148 |
-
|
| 149 |
-
# Extract degrees and institutions
|
| 150 |
-
for keyword in self.education_keywords:
|
| 151 |
-
if keyword in text_lower:
|
| 152 |
-
# Find context around the keyword
|
| 153 |
-
pattern = rf'.{{0,50}}{re.escape(keyword)}.{{0,50}}'
|
| 154 |
-
matches = re.findall(pattern, text, re.IGNORECASE)
|
| 155 |
-
education.extend(matches)
|
| 156 |
-
|
| 157 |
-
return education[:3] # Return top 3 education entries
|
| 158 |
-
|
| 159 |
-
def extract_experience(self, text):
|
| 160 |
-
"""Extract work experience"""
|
| 161 |
-
experience = []
|
| 162 |
-
lines = text.split('\n')
|
| 163 |
-
|
| 164 |
-
# Look for experience section
|
| 165 |
-
in_experience_section = False
|
| 166 |
-
current_experience = ""
|
| 167 |
-
|
| 168 |
-
for line in lines:
|
| 169 |
-
line_lower = line.lower()
|
| 170 |
-
if any(keyword in line_lower for keyword in ['experience', 'work', 'employment', 'career']):
|
| 171 |
-
in_experience_section = True
|
| 172 |
-
continue
|
| 173 |
-
elif in_experience_section and any(keyword in line_lower for keyword in ['education', 'skill', 'project']):
|
| 174 |
-
if current_experience:
|
| 175 |
-
experience.append(current_experience.strip())
|
| 176 |
-
break
|
| 177 |
-
elif in_experience_section:
|
| 178 |
-
if line.strip():
|
| 179 |
-
current_experience += line + " "
|
| 180 |
-
elif current_experience:
|
| 181 |
-
experience.append(current_experience.strip())
|
| 182 |
-
current_experience = ""
|
| 183 |
-
|
| 184 |
-
if current_experience:
|
| 185 |
-
experience.append(current_experience.strip())
|
| 186 |
-
|
| 187 |
-
return experience[:3] # Return top 3 experience entries
|
| 188 |
-
|
| 189 |
-
def extract_summary(self, text):
|
| 190 |
-
"""Extract summary/objective"""
|
| 191 |
-
lines = text.split('\n')
|
| 192 |
-
summary = ""
|
| 193 |
-
|
| 194 |
-
for i, line in enumerate(lines):
|
| 195 |
-
line_lower = line.lower()
|
| 196 |
-
if any(keyword in line_lower for keyword in ['summary', 'objective', 'profile', 'about']):
|
| 197 |
-
# Get next few lines as summary
|
| 198 |
-
summary_lines = lines[i+1:i+4]
|
| 199 |
-
summary = ' '.join([l.strip() for l in summary_lines if l.strip()])
|
| 200 |
-
break
|
| 201 |
-
|
| 202 |
-
return summary[:200] # Limit to 200 characters
|
| 203 |
-
|
| 204 |
-
def extract_resume_features(file_path):
|
| 205 |
-
"""
|
| 206 |
-
Main function to extract features from resume
|
| 207 |
-
Returns a dictionary with extracted information
|
| 208 |
-
"""
|
| 209 |
-
try:
|
| 210 |
-
parser = SimpleResumeParser()
|
| 211 |
-
text = parser.extract_text(file_path)
|
| 212 |
-
|
| 213 |
-
if not text:
|
| 214 |
-
return {
|
| 215 |
-
'name': '',
|
| 216 |
-
'email': '',
|
| 217 |
-
'mobile_number': '',
|
| 218 |
-
'skills': [],
|
| 219 |
-
'experience': [],
|
| 220 |
-
'education': [],
|
| 221 |
-
'summary': ''
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
# Extract all features
|
| 225 |
-
features = {
|
| 226 |
-
'name': parser.extract_name(text),
|
| 227 |
-
'email': parser.extract_email(text),
|
| 228 |
-
'mobile_number': parser.extract_phone(text),
|
| 229 |
-
'skills': parser.extract_skills(text),
|
| 230 |
-
'experience': parser.extract_experience(text),
|
| 231 |
-
'education': parser.extract_education(text),
|
| 232 |
-
'summary': parser.extract_summary(text)
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
return features
|
| 236 |
-
|
| 237 |
-
except Exception as e:
|
| 238 |
-
print(f"Error extracting resume features: {e}")
|
| 239 |
-
return {
|
| 240 |
-
'name': '',
|
| 241 |
-
'email': '',
|
| 242 |
-
'mobile_number': '',
|
| 243 |
-
'skills': [],
|
| 244 |
-
'experience': [],
|
| 245 |
-
'education': [],
|
| 246 |
-
'summary': ''
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
# For backward compatibility
|
| 250 |
-
def parse_resume(file_path):
|
| 251 |
-
return extract_resume_features(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/routes/auth.py
CHANGED
|
@@ -14,25 +14,42 @@ auth_bp = Blueprint('auth', __name__)
|
|
| 14 |
|
| 15 |
def handle_resume_upload(file):
|
| 16 |
"""
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"""
|
|
|
|
| 20 |
if not file or file.filename == '':
|
| 21 |
return None, "No file uploaded", None
|
| 22 |
|
| 23 |
try:
|
| 24 |
-
#
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
filename = secure_filename(file.filename)
|
| 27 |
-
|
| 28 |
-
os.makedirs(
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
|
|
|
|
|
|
| 36 |
print(f"Error in handle_resume_upload: {e}")
|
| 37 |
return None, str(e), None
|
| 38 |
|
|
|
|
| 14 |
|
| 15 |
def handle_resume_upload(file):
|
| 16 |
"""
|
| 17 |
+
Handle a resume upload by saving the file to a temporary location and
|
| 18 |
+
returning an empty feature dictionary. This function no longer attempts
|
| 19 |
+
to parse the resume contents; it simply stores the file so that it can
|
| 20 |
+
be referenced later (e.g. for downloading or inspection) and returns
|
| 21 |
+
a placeholder features object. A tuple of (features_dict, error_message,
|
| 22 |
+
filepath) is returned. If an error occurs, ``features_dict`` will be
|
| 23 |
+
``None`` and ``error_message`` will contain a description of the error.
|
| 24 |
"""
|
| 25 |
+
# Validate that a file was provided
|
| 26 |
if not file or file.filename == '':
|
| 27 |
return None, "No file uploaded", None
|
| 28 |
|
| 29 |
try:
|
| 30 |
+
# Generate a safe filename and determine the target directory. Use
|
| 31 |
+
# ``/tmp/resumes`` so that the directory is writable within Hugging
|
| 32 |
+
# Face Spaces. Creating the directory with ``exist_ok=True`` ensures
|
| 33 |
+
# that it is available without raising an error if it already exists.
|
| 34 |
filename = secure_filename(file.filename)
|
| 35 |
+
temp_dir = os.path.join('/tmp', 'resumes')
|
| 36 |
+
os.makedirs(temp_dir, exist_ok=True)
|
| 37 |
+
filepath = os.path.join(temp_dir, filename)
|
| 38 |
|
| 39 |
+
# Save the uploaded file to the temporary location. If this fails
|
| 40 |
+
# (e.g. due to permissions issues) the exception will be caught below.
|
| 41 |
+
file.save(filepath)
|
| 42 |
|
| 43 |
+
# Resume parsing has been removed from this project. To maintain
|
| 44 |
+
# compatibility with the rest of the application, return an empty
|
| 45 |
+
# dictionary of features. Downstream code will interpret an empty
|
| 46 |
+
# dictionary as "no extracted features", which still allows the
|
| 47 |
+
# interview flow to proceed.
|
| 48 |
+
features = {}
|
| 49 |
+
return features, None, filepath
|
| 50 |
except Exception as e:
|
| 51 |
+
# Log the error and propagate it to the caller. Returning None for
|
| 52 |
+
# ``features`` signals that the upload failed.
|
| 53 |
print(f"Error in handle_resume_upload: {e}")
|
| 54 |
return None, str(e), None
|
| 55 |
|
backend/services/interview_engine.py
CHANGED
|
@@ -11,13 +11,43 @@ import torch
|
|
| 11 |
|
| 12 |
# Initialize models
|
| 13 |
chat_groq_api = os.getenv("GROQ_API_KEY")
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Initialize Whisper model
|
| 23 |
#
|
|
|
|
| 11 |
|
| 12 |
# Initialize models
|
| 13 |
chat_groq_api = os.getenv("GROQ_API_KEY")
|
| 14 |
+
|
| 15 |
+
# Attempt to initialize the Groq LLM only if an API key is provided. When
|
| 16 |
+
# running in environments where the key is unavailable (such as local
|
| 17 |
+
# development or automated testing), fall back to a simple stub that
|
| 18 |
+
# generates generic responses. This avoids raising an exception at import
|
| 19 |
+
# time and allows the rest of the application to run without external
|
| 20 |
+
# dependencies. See the DummyGroq class defined below.
|
| 21 |
+
if chat_groq_api:
|
| 22 |
+
try:
|
| 23 |
+
groq_llm = ChatGroq(
|
| 24 |
+
temperature=0.7,
|
| 25 |
+
model_name="llama-3.3-70b-versatile",
|
| 26 |
+
api_key=chat_groq_api
|
| 27 |
+
)
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logging.error(f"Error initializing ChatGroq LLM: {e}. Falling back to dummy model.")
|
| 30 |
+
groq_llm = None
|
| 31 |
+
else:
|
| 32 |
+
groq_llm = None
|
| 33 |
+
|
| 34 |
+
if groq_llm is None:
|
| 35 |
+
class DummyGroq:
|
| 36 |
+
"""A fallback language model used when no Groq API key is set.
|
| 37 |
+
|
| 38 |
+
The ``invoke`` method of this class returns a simple canned response
|
| 39 |
+
rather than calling an external API. This ensures that the
|
| 40 |
+
interview functionality still produces a sensible prompt, albeit
|
| 41 |
+
without advanced LLM behaviour.
|
| 42 |
+
"""
|
| 43 |
+
def invoke(self, prompt: str):
|
| 44 |
+
# Provide a very generic question based on the prompt. This
|
| 45 |
+
# implementation ignores the prompt contents entirely; in a more
|
| 46 |
+
# sophisticated fallback you could parse ``prompt`` to tailor
|
| 47 |
+
# responses.
|
| 48 |
+
return "Tell me about yourself and why you're interested in this position."
|
| 49 |
+
|
| 50 |
+
groq_llm = DummyGroq()
|
| 51 |
|
| 52 |
# Initialize Whisper model
|
| 53 |
#
|
backend/templates/apply.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
| 25 |
<div class="card">
|
| 26 |
<div class="card-header">
|
| 27 |
<h2>Submit Your Application</h2>
|
| 28 |
-
<p>Please upload your resume (PDF, DOCX).
|
| 29 |
</div>
|
| 30 |
|
| 31 |
<div class="card-body">
|
|
|
|
| 25 |
<div class="card">
|
| 26 |
<div class="card-header">
|
| 27 |
<h2>Submit Your Application</h2>
|
| 28 |
+
<p>Please upload your resume (PDF, DOCX). Your file will be saved securely for recruiters to review.</p>
|
| 29 |
</div>
|
| 30 |
|
| 31 |
<div class="card-body">
|
backend/utils/luna_phase1.py
DELETED
|
File without changes
|
requirements.txt
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
|
|
| 1 |
flask
|
| 2 |
-
scikit-learn
|
| 3 |
-
pandas
|
| 4 |
-
joblib
|
| 5 |
-
PyMuPDF
|
| 6 |
-
python-docx
|
| 7 |
-
spacy>=3.0.0
|
| 8 |
-
nltk
|
| 9 |
-
pyresparser
|
| 10 |
flask_login
|
| 11 |
flask_sqlalchemy
|
| 12 |
flask_wtf
|
|
@@ -36,9 +29,7 @@ cohere==5.16.1
|
|
| 36 |
# Vector DB
|
| 37 |
qdrant-client==1.14.3
|
| 38 |
|
| 39 |
-
# PDF & DOCX parsing
|
| 40 |
-
PyPDF2==3.0.1
|
| 41 |
-
python-docx==1.2.0
|
| 42 |
|
| 43 |
# Audio processing
|
| 44 |
ffmpeg-python==0.2.0
|
|
@@ -46,7 +37,7 @@ inputimeout==1.0.4
|
|
| 46 |
evaluate==0.4.5
|
| 47 |
accelerate==0.29.3
|
| 48 |
huggingface_hub==0.20.3
|
| 49 |
-
textract
|
| 50 |
bitsandbytes
|
| 51 |
faster-whisper==0.10.0
|
| 52 |
edge-tts==6.1.2
|
|
|
|
| 1 |
+
|
| 2 |
flask
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
flask_login
|
| 4 |
flask_sqlalchemy
|
| 5 |
flask_wtf
|
|
|
|
| 29 |
# Vector DB
|
| 30 |
qdrant-client==1.14.3
|
| 31 |
|
| 32 |
+
# PDF & DOCX parsing (removed; resume parsing is no longer supported)
|
|
|
|
|
|
|
| 33 |
|
| 34 |
# Audio processing
|
| 35 |
ffmpeg-python==0.2.0
|
|
|
|
| 37 |
evaluate==0.4.5
|
| 38 |
accelerate==0.29.3
|
| 39 |
huggingface_hub==0.20.3
|
| 40 |
+
# textract removed; no resume parsing
|
| 41 |
bitsandbytes
|
| 42 |
faster-whisper==0.10.0
|
| 43 |
edge-tts==6.1.2
|