import streamlit as st import PyPDF2 import re import nltk from nltk.tokenize import word_tokenize from nltk.corpus import wordnet import requests from typing import Optional import pandas as pd from sqlalchemy import create_engine, Column, Integer, String, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker import json import openai # Import OpenAI # Initialize NLTK resources def download_nltk_resources(): resources = { 'punkt': 'tokenizers/punkt', 'averaged_perceptron_tagger': 'taggers/averaged_perceptron_tagger', 'wordnet': 'corpora/wordnet', 'stopwords': 'corpora/stopwords' } for package, resource in resources.items(): try: nltk.data.find(resource) except LookupError: nltk.download(package) download_nltk_resources() # Ensure spaCy model is downloaded import spacy try: nlp = spacy.load("en_core_web_sm") except OSError: spacy.cli.download("en_core_web_sm") nlp = spacy.load("en_core_web_sm") # Database setup Base = declarative_base() class ResumeScore(Base): __tablename__ = 'resume_scores' id = Column(Integer, primary_key=True) resume_name = Column(String) score = Column(Float) skills = Column(String) certifications = Column(String) experience_years = Column(Float) education_level = Column(String) summary = Column(String) # Create engine and session engine = create_engine('sqlite:///resumes.db') Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() # Custom CSS to enhance UI def set_custom_css(): st.markdown(""" """, unsafe_allow_html=True) def get_docparser_data(file, api_key, parser_id) -> Optional[dict]: # First, upload the document upload_url = "https://api.docparser.com/v1/document/upload" headers = { 'Authorization': f'Basic {api_key}' } files = { 'file': (file.name, file, 'application/pdf') } params = { 'parser_id': parser_id } try: # Upload document upload_response = requests.post( upload_url, headers=headers, files=files, params=params ) upload_response.raise_for_status() # Get document ID from upload response upload_data = upload_response.json() if not isinstance(upload_data, list) or len(upload_data) == 0: st.error("Invalid response from Docparser upload") return None document_id = upload_data[0].get('id') if not document_id: st.error("Failed to get document ID from upload response") return None # Get parsed results results_url = f"https://api.docparser.com/v1/results/{parser_id}/{document_id}" results_response = requests.get( results_url, headers=headers ) results_response.raise_for_status() # Handle results results_data = results_response.json() if isinstance(results_data, list) and len(results_data) > 0: return results_data[0] else: st.error("No parsed data received from Docparser") return None except requests.exceptions.HTTPError as http_err: st.error(f"HTTP error occurred: {http_err}") if http_err.response is not None: st.error(f"Response content: {http_err.response.content}") except Exception as e: st.error(f"Error fetching data from Docparser: {e}") return None def get_openai_data(file, openai_key: str) -> Optional[dict]: openai.api_key = openai_key try: file_content = file.read() response = openai.Completion.create( engine="text-davinci-003", prompt=f"Extract and analyze the resume content: {file_content}", max_tokens=1500 ) return response.choices[0].text except Exception as e: st.error(f"Error fetching data from OpenAI: {e}") return None def calculate_weighted_score(skills, certifications, experience_years, education_level, projects, skill_weight, certification_weight, experience_weight, education_weight, project_weight): skill_score = min(len(skills) * 15, 100) certification_score = min(len(certifications) * 20, 100) experience_score = min(experience_years * 15, 100) education_score = 100 if education_level else 0 project_score = min(len(projects) * 10, 100) # Assuming each project contributes 10 points total_score = ( skill_score * skill_weight + certification_score * certification_weight + experience_score * experience_weight + education_score * education_weight + project_score * project_weight ) return round(min(total_score, 100), 2) def process_resume(file, job_description, filename, parser_choice, openai_key=None, api_key=None, parser_id=None, skill_weight=0.9, certification_weight=0.05, experience_weight=0.03, education_weight=0.02, project_weight=0.1): try: if parser_choice == "Docparser": data = get_docparser_data(file, api_key, parser_id) elif parser_choice == "OpenAI": data = get_openai_data(file, openai_key) else: st.error("Invalid parser choice") return None if not data: st.warning(f"Failed to extract data from the resume {filename}") return None # Extract fields from the response personal_details = { 'name': data.get('name', 'Unknown'), 'email': data.get('email', 'Unknown'), 'phone': data.get('phone', 'Unknown') } education = { 'degree': data.get('degree', 'Not specified'), 'institution': data.get('institution', 'Not specified'), 'year': data.get('year', 'Not specified') } experience_years = data.get('experience_years', 0) # Ensure certifications, skills, and projects are lists of strings certifications = [cert if isinstance(cert, str) else str(cert) for cert in data.get('certifications', [])] skills = [skill if isinstance(skill, str) else str(skill) for skill in data.get('skills', [])] projects = [project if isinstance(project, str) else str(project) for project in data.get('projects', [])] # Assuming 'projects' is a key in the data summary = data.get('summary', 'No summary available') # Calculate weighted score weighted_score = calculate_weighted_score( skills, certifications, experience_years, education.get('degree', 'Not specified'), projects, skill_weight, certification_weight, experience_weight, education_weight, project_weight ) resume_name = filename or personal_details.get('name', 'Unknown') skills_str = ', '.join(skills) certifications_str = ', '.join(certifications) projects_str = ', '.join(projects) resume_score = ResumeScore( resume_name=resume_name, score=weighted_score, skills=skills_str, certifications=certifications_str, experience_years=experience_years, education_level=education.get('degree', 'Not specified'), summary=summary ) session.add(resume_score) session.commit() result = { 'name': resume_name, 'score': weighted_score, 'personal_details': personal_details, 'education': education, 'experience': {'total_years': experience_years}, 'certifications': certifications, 'skills': skills, 'projects': projects, # Include projects in the result 'summary': summary } return result except Exception as e: st.error(f"Error processing the resume {filename}: {e}") session.rollback() return None def process_resumes(files, job_description, parser_choice, openai_key=None, api_key=None, parser_id=None, skill_weight=0.9, certification_weight=0.05, experience_weight=0.03, education_weight=0.02, project_weight=0.1): scores = [] processed_count = 0 try: if not files: st.warning("No PDF files uploaded") return [] total_files = len(files) progress_bar = st.progress(0) for index, file in enumerate(files): result = process_resume(file, job_description, file.name, parser_choice, openai_key, api_key, parser_id, skill_weight, certification_weight, experience_weight, education_weight, project_weight) if result: scores.append(result) processed_count += 1 progress = (index + 1) / total_files progress_bar.progress(progress) st.success(f"Successfully processed {processed_count} resumes") return scores except Exception as e: st.error(f"Error processing resumes: {e}") session.rollback() return [] def display_results(result): with st.expander(f"📄 {result.get('name', 'Unknown')} - Match: {result['score']}%"): st.write(f"### Overall Match Score: {result['score']}%") st.write("### Skills Found:") if result['skills']: for skill in result['skills']: st.markdown(f"- {skill}") else: st.markdown("No skills found.") st.write("### Certifications:") if result['certifications']: for cert in result['certifications']: st.markdown(f"- {cert}") else: st.markdown("No certifications found.") st.write(f"### Total Years of Experience: {result['experience'].get('total_years', 0)}") st.write("### Education:") degree = result['education'].get('degree', 'Not specified') st.markdown(f"- Degree: {degree}") if st.button(f"View Detailed Analysis ({result.get('name', 'Unknown')})", key=f"view_{result.get('name', 'default')}"): st.write("#### Resume Summary:") st.text(result['summary']) def view_scores(): st.header("Stored Resume Scores") resumes = session.query(ResumeScore).order_by(ResumeScore.score.desc()).all() if resumes: data = [] for idx, resume in enumerate(resumes, start=1): try: # Attempt to parse skills and certifications as JSON skills = json.loads(resume.skills) certifications = json.loads(resume.certifications) # Extract values if they are in JSON format skills_str = ', '.join([skill['key_0'] for skill in skills]) if isinstance(skills, list) else resume.skills certifications_str = ', '.join([cert['key_0'] for cert in certifications]) if isinstance(certifications, list) else resume.certifications except json.JSONDecodeError: # If parsing fails, treat them as plain strings skills_str = resume.skills certifications_str = resume.certifications data.append({ 'S.No': idx, 'Name': resume.resume_name, 'Score': resume.score, 'Skills': skills_str, 'Certifications': certifications_str, 'Experience (Years)': resume.experience_years, 'Education': resume.education_level, 'Projects': resume.summary # Assuming projects are part of the summary or add a separate field if needed }) df = pd.DataFrame(data) df_display = df[['S.No', 'Name', 'Score', 'Skills', 'Certifications', 'Experience (Years)', 'Education', 'Projects']] # Define a threshold for best-fit resumes threshold = 50 best_fits = df[df['Score'] >= threshold] # Display all resumes st.subheader("All Resumes") for index, row in df_display.iterrows(): st.write(f"**{row['S.No']}. {row['Name']}**") st.write(f"Score: {row['Score']}%") st.write(f"Skills: {row['Skills']}") st.write(f"Certifications: {row['Certifications']}") st.write(f"Experience: {row['Experience (Years)']} years") st.write(f"Education: {row['Education']}") st.write(f"Projects: {row['Projects']}") col1, col2 = st.columns([1, 1]) with col1: if st.button(f"View Detailed Analysis ({row['Name']})", key=f"view_{index}"): st.write(f"## Analysis Report for {row['Name']}") st.write(f"### Score: {row['Score']}%") st.write(f"### Skills: {row['Skills']}") st.write(f"### Certifications: {row['Certifications']}") st.write(f"### Experience: {row['Experience (Years)']} years") st.write(f"### Education: {row['Education']}") st.write("### Projects:") st.text(row['Projects']) with col2: if st.button(f"Delete {row['Name']}", key=f"delete_{index}"): # Find the resume in the database and delete it resume_to_delete = session.query(ResumeScore).filter_by(resume_name=row['Name']).first() if resume_to_delete: session.delete(resume_to_delete) session.commit() st.success(f"Deleted {row['Name']} from the database.") st.experimental_set_query_params(refresh=True) # Use query params to trigger a rerun # Display best-fit resumes if not best_fits.empty: st.subheader("Best Fit Resumes") for index, row in best_fits.iterrows(): st.write(f"**{row['S.No']}. {row['Name']}**") st.write(f"Score: {row['Score']}%") st.write(f"Skills: {row['Skills']}") st.write(f"Certifications: {row['Certifications']}") st.write(f"Experience: {row['Experience (Years)']} years") st.write(f"Education: {row['Education']}") st.write(f"Projects: {row['Projects']}") col1, col2 = st.columns([1, 1]) with col1: if st.button(f"View Detailed Analysis ({row['Name']})", key=f"view_best_{index}"): st.write(f"## Analysis Report for {row['Name']}") st.write(f"### Score: {row['Score']}%") st.write(f"### Skills: {row['Skills']}") st.write(f"### Certifications: {row['Certifications']}") st.write(f"### Experience: {row['Experience (Years)']} years") st.write(f"### Education: {row['Education']}") st.write("### Projects:") st.text(row['Projects']) with col2: if st.button(f"Delete {row['Name']}", key=f"delete_best_{index}"): # Find the resume in the database and delete it resume_to_delete = session.query(ResumeScore).filter_by(resume_name=row['Name']).first() if resume_to_delete: session.delete(resume_to_delete) session.commit() st.success(f"Deleted {row['Name']} from the database.") st.experimental_set_query_params(refresh=True) # Use query params to trigger a rerun else: st.write("No resume scores available.") def main(): st.title("Resume Analyzer") set_custom_css() menu = ["Home", "View Scores"] choice = st.sidebar.selectbox("Menu", menu) if choice == "Home": analysis_type = st.selectbox("Select Analysis Type:", ["Single Resume", "Folder Upload"]) method_choice = st.selectbox("Select Method:", ["Use LLM", "Use Field Extraction"]) openai_key = None # Initialize openai_key if method_choice == "Use LLM": openai_key = st.text_input("Enter OpenAI API Key:", type="password") parser_choice = "OpenAI" else: parser_choice = "Docparser" # Only Docparser is available for field extraction api_key = st.text_input("Enter Docparser API Key:", type="password") parser_id = st.text_input("Enter Docparser Parser ID:") job_description = st.text_area("Enter job description:", height=150, placeholder="Paste job description here...", key="job_desc") # Configure weights st.sidebar.header("Configure Weights") skill_weight = st.sidebar.slider("Skill Weight", 0.0, 1.0, 0.9) certification_weight = st.sidebar.slider("Certification Weight", 0.0, 1.0, 0.05) experience_weight = st.sidebar.slider("Experience Weight", 0.0, 1.0, 0.03) education_weight = st.sidebar.slider("Education Weight", 0.0, 1.0, 0.02) project_weight = st.sidebar.slider("Project Weight", 0.0, 1.0, 0.1) # New slider for project weight if analysis_type == "Single Resume": uploaded_file = st.file_uploader("Upload a resume PDF file", type="pdf") if st.button("Analyze Resume"): if not uploaded_file: st.error("Please upload a resume PDF file") return if not job_description: st.error("Please enter a job description") return if method_choice == "Use LLM" and not openai_key: st.error("Please enter the OpenAI API key") return if method_choice == "Use Field Extraction" and (not api_key or not parser_id): st.error("Please enter the Docparser API key and Parser ID") return with st.spinner("Processing resume..."): result = process_resume(uploaded_file, job_description, uploaded_file.name, parser_choice, openai_key, api_key, parser_id, skill_weight, certification_weight, experience_weight, education_weight, project_weight) if result: st.success("Analysis complete!") display_results(result) else: st.warning("Failed to process the resume.") elif analysis_type == "Folder Upload": uploaded_files = st.file_uploader("Upload multiple resume PDF files", type="pdf", accept_multiple_files=True) if st.button("Analyze Resumes"): if not uploaded_files: st.error("Please upload resume PDF files") return if not job_description: st.error("Please enter a job description") return if method_choice == "Use LLM" and not openai_key: st.error("Please enter the OpenAI API key") return if method_choice == "Use Field Extraction" and (not api_key or not parser_id): st.error("Please enter the Docparser API key and Parser ID") return with st.spinner("Processing resumes..."): scores = process_resumes(uploaded_files, job_description, parser_choice, openai_key, api_key, parser_id, skill_weight, certification_weight, experience_weight, education_weight, project_weight) if scores: st.success("Analysis complete!") for result in scores: display_results(result) else: st.warning("No valid resumes found to process") with st.expander("ℹī¸ How to use"): st.markdown(""" 1. Select the analysis type: Single Resume or Folder Upload. 2. Choose the method: Use LLM or Use Field Extraction. 3. If using LLM, enter the OpenAI API key. 4. If using Field Extraction, enter the Docparser API key and Parser ID. 5. Upload a resume PDF file or multiple files. 6. Paste the job description. 7. Configure the weights for skills, certifications, experience, education, and projects. 8. Click 'Analyze' to start processing. 9. View the match score and extracted information. 10. Click 'View Detailed Analysis' to see the summary and more details. """) elif choice == "View Scores": view_scores() if __name__ == "__main__": main()