| import os |
| import gradio as gr |
| import PyPDF2 |
| import docx2txt |
| import logging |
| import json |
| from typing import List, Dict, Tuple |
| import uuid |
| from datetime import datetime |
|
|
| import nltk |
| from nltk.corpus import stopwords |
| from nltk.tokenize import word_tokenize |
|
|
| from sklearn.feature_extraction.text import TfidfVectorizer |
| from sklearn.metrics.pairwise import cosine_similarity |
|
|
| |
| PINECONE_AVAILABLE = False |
| try: |
| from pinecone import Pinecone, ServerlessSpec |
| PINECONE_AVAILABLE = True |
| print("β
Pinecone library loaded successfully") |
| except ImportError: |
| print("β οΈ Pinecone not available - using in-memory storage") |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
| |
| try: |
| nltk.download('punkt', quiet=True) |
| nltk.download('stopwords', quiet=True) |
| nltk.download('punkt_tab', quiet=True) |
| except: |
| pass |
|
|
| |
| |
| |
|
|
| def check_pinecone_setup(): |
| """Check if Pinecone is properly configured.""" |
| api_key = os.getenv("PINECONE_API_KEY") |
| |
| if not api_key: |
| return False, "No PINECONE_API_KEY found in environment variables" |
| |
| if not PINECONE_AVAILABLE: |
| return False, "Pinecone library not installed" |
| |
| try: |
| |
| pc = Pinecone(api_key=api_key) |
| pc.list_indexes() |
| return True, "Pinecone connection successful" |
| except Exception as e: |
| return False, f"Pinecone connection failed: {str(e)}" |
|
|
| |
| |
| |
|
|
| class PineconeResumeDB: |
| def __init__(self, api_key: str): |
| """Initialize Pinecone connection and index.""" |
| self.pc = Pinecone(api_key=api_key) |
| self.index_name = "resume-vectors" |
| self.dimension = 1000 |
| |
| |
| existing_indexes = [index.name for index in self.pc.list_indexes()] |
| if self.index_name not in existing_indexes: |
| print(f"Creating new Pinecone index: {self.index_name}") |
| self.pc.create_index( |
| name=self.index_name, |
| dimension=self.dimension, |
| metric="cosine", |
| spec=ServerlessSpec( |
| cloud="aws", |
| region="us-east-1" |
| ) |
| ) |
| |
| self.index = self.pc.Index(self.index_name) |
| self.vectorizer = TfidfVectorizer(max_features=self.dimension) |
| self.is_fitted = False |
| |
| def add_resume(self, resume_id: str, text_content: str, metadata: Dict): |
| """Store a resume vector in Pinecone with metadata.""" |
| try: |
| processed_text = preprocess_text(text_content) |
| |
| |
| |
| if not self.is_fitted: |
| |
| vector = self.vectorizer.fit_transform([processed_text]).toarray()[0] |
| self.is_fitted = True |
| else: |
| |
| |
| vector = self.vectorizer.fit_transform([processed_text]).toarray()[0] |
| |
| |
| full_metadata = { |
| **metadata, |
| "text_preview": text_content[:500], |
| "upload_date": datetime.now().isoformat(), |
| "vector_model": "tfidf" |
| } |
| |
| |
| self.index.upsert( |
| vectors=[{ |
| "id": resume_id, |
| "values": vector.tolist(), |
| "metadata": full_metadata |
| }] |
| ) |
| |
| logging.info(f"Successfully stored resume {resume_id} in Pinecone") |
| return True |
| |
| except Exception as e: |
| logging.error(f"Error storing resume in Pinecone: {e}") |
| return False |
| |
| def search_resumes(self, job_description: str, top_k: int = 10) -> List[Tuple[str, float, Dict]]: |
| """Search for most similar resumes to a job description.""" |
| try: |
| processed_jd = preprocess_text(job_description) |
| |
| if not self.is_fitted: |
| return [] |
| |
| jd_vector = self.vectorizer.transform([processed_jd]).toarray()[0] |
| |
| |
| results = self.index.query( |
| vector=jd_vector.tolist(), |
| top_k=top_k, |
| include_metadata=True, |
| include_values=False |
| ) |
| |
| |
| formatted_results = [] |
| for match in results.matches: |
| formatted_results.append(( |
| match.id, |
| match.score, |
| match.metadata |
| )) |
| |
| return formatted_results |
| |
| except Exception as e: |
| logging.error(f"Error searching Pinecone: {e}") |
| return [] |
| |
| def get_resume_count(self) -> int: |
| """Get total number of resumes in the database.""" |
| try: |
| stats = self.index.describe_index_stats() |
| return stats.total_vector_count |
| except: |
| return 0 |
| |
| def clear_database(self): |
| """Clear all vectors from the database.""" |
| try: |
| |
| self.index.delete(delete_all=True) |
| return True |
| except Exception as e: |
| logging.error(f"Error clearing database: {e}") |
| return False |
|
|
| |
| |
| |
|
|
| class InMemoryResumeDB: |
| def __init__(self): |
| """Initialize in-memory storage for fallback.""" |
| self.resumes = {} |
| self.vectorizer = None |
| self.resume_vectors = {} |
| |
| def add_resume(self, resume_id: str, text_content: str, metadata: Dict): |
| """Store a resume in memory.""" |
| try: |
| processed_text = preprocess_text(text_content) |
| |
| self.resumes[resume_id] = { |
| 'text': processed_text, |
| 'raw_text': text_content, |
| 'metadata': { |
| **metadata, |
| 'upload_date': datetime.now().isoformat(), |
| 'text_length': len(text_content) |
| } |
| } |
| |
| self._update_vectors() |
| return True |
| |
| except Exception as e: |
| logging.error(f"Error storing resume: {e}") |
| return False |
| |
| def _update_vectors(self): |
| """Update TF-IDF vectors for all resumes.""" |
| if not self.resumes: |
| return |
| |
| try: |
| texts = [resume['text'] for resume in self.resumes.values()] |
| resume_ids = list(self.resumes.keys()) |
| |
| self.vectorizer = TfidfVectorizer(max_features=1000) |
| vectors = self.vectorizer.fit_transform(texts) |
| |
| for i, resume_id in enumerate(resume_ids): |
| self.resume_vectors[resume_id] = vectors[i] |
| |
| except Exception as e: |
| logging.error(f"Error updating vectors: {e}") |
| |
| def search_resumes(self, job_description: str, top_k: int = 10) -> List[Tuple[str, float, Dict]]: |
| """Search for most similar resumes.""" |
| try: |
| if not self.resumes or not self.vectorizer: |
| return [] |
| |
| processed_jd = preprocess_text(job_description) |
| jd_vector = self.vectorizer.transform([processed_jd]) |
| |
| results = [] |
| for resume_id, resume_vector in self.resume_vectors.items(): |
| similarity = cosine_similarity(jd_vector, resume_vector)[0][0] |
| results.append((resume_id, similarity, self.resumes[resume_id]['metadata'])) |
| |
| results.sort(key=lambda x: x[1], reverse=True) |
| return results[:top_k] |
| |
| except Exception as e: |
| logging.error(f"Error searching resumes: {e}") |
| return [] |
| |
| def get_resume_count(self) -> int: |
| return len(self.resumes) |
| |
| def clear_database(self): |
| self.resumes.clear() |
| self.resume_vectors.clear() |
| self.vectorizer = None |
| return True |
|
|
| |
| |
| |
|
|
| |
| pinecone_available, pinecone_status = check_pinecone_setup() |
| print(f"Pinecone status: {pinecone_status}") |
|
|
| if pinecone_available: |
| api_key = os.getenv("PINECONE_API_KEY") |
| resume_db = PineconeResumeDB(api_key=api_key) |
| DB_TYPE = "Pinecone Vector Database" |
| else: |
| resume_db = InMemoryResumeDB() |
| DB_TYPE = "In-Memory Storage (Demo Mode)" |
|
|
| print(f"Using database type: {DB_TYPE}") |
|
|
| |
| |
| |
|
|
| def extract_text_from_pdf(file_obj): |
| """Extract all text from a PDF file object.""" |
| text_content = [] |
| try: |
| if hasattr(file_obj, 'read'): |
| pdf_reader = PyPDF2.PdfReader(file_obj) |
| else: |
| with open(file_obj, 'rb') as f: |
| pdf_reader = PyPDF2.PdfReader(f) |
| |
| for page in pdf_reader.pages: |
| page_text = page.extract_text() |
| if page_text: |
| text_content.append(page_text) |
| return "\n".join(text_content) |
| except Exception as e: |
| logging.error(f"Error reading PDF: {e}") |
| return f"Error reading PDF: {str(e)}" |
|
|
| def extract_text_from_docx(file_path): |
| """Extract all text from a DOCX file.""" |
| try: |
| return docx2txt.process(file_path) |
| except Exception as e: |
| logging.error(f"Error reading DOCX: {e}") |
| return f"Error reading DOCX: {str(e)}" |
|
|
| def extract_text_from_txt(file_obj): |
| """Extract all text from a TXT file.""" |
| try: |
| if hasattr(file_obj, 'read'): |
| content = file_obj.read() |
| if isinstance(content, bytes): |
| return content.decode("utf-8", errors="ignore") |
| return content |
| else: |
| with open(file_obj, 'r', encoding='utf-8', errors='ignore') as f: |
| return f.read() |
| except Exception as e: |
| logging.error(f"Error reading TXT: {e}") |
| return f"Error reading TXT: {str(e)}" |
|
|
| def preprocess_text(text): |
| """Clean and preprocess text.""" |
| try: |
| text = str(text).lower() |
| tokens = word_tokenize(text) |
| stop_words = set(stopwords.words('english')) |
| filtered_tokens = [t for t in tokens if t.isalpha() and t not in stop_words and len(t) > 2] |
| return " ".join(filtered_tokens) |
| except Exception as e: |
| logging.error(f"Error preprocessing text: {e}") |
| return str(text).lower() |
|
|
| |
| |
| |
|
|
| def add_resumes_to_db(cv_files, position_title="General"): |
| """Add uploaded resumes to database.""" |
| if not cv_files: |
| return "β No files uploaded." |
| |
| added_count = 0 |
| results = [f"ποΈ Using: {DB_TYPE}\n"] |
| |
| for uploaded_file in cv_files: |
| try: |
| filename = uploaded_file.name if hasattr(uploaded_file, 'name') else str(uploaded_file) |
| filename = os.path.basename(filename) |
| file_ext = os.path.splitext(filename)[1].lower() |
| |
| |
| if file_ext == ".pdf": |
| file_content = extract_text_from_pdf(uploaded_file) |
| elif file_ext == ".txt": |
| file_content = extract_text_from_txt(uploaded_file) |
| elif file_ext == ".docx": |
| file_content = extract_text_from_docx(uploaded_file) |
| else: |
| results.append(f"β {filename}: Unsupported file type") |
| continue |
| |
| if "Error reading" in file_content: |
| results.append(f"β {filename}: {file_content}") |
| continue |
| |
| if len(file_content.strip()) < 50: |
| results.append(f"β {filename}: File appears to be empty or too short") |
| continue |
| |
| |
| resume_id = f"{filename}_{uuid.uuid4().hex[:8]}" |
| |
| |
| metadata = { |
| "filename": filename, |
| "file_type": file_ext, |
| "position_applied": position_title, |
| } |
| |
| |
| if resume_db.add_resume(resume_id, file_content, metadata): |
| added_count += 1 |
| results.append(f"β
{filename}: Successfully added to database") |
| else: |
| results.append(f"β {filename}: Failed to add to database") |
| |
| except Exception as e: |
| results.append(f"β Error processing file: {str(e)}") |
| |
| summary = f"\nπ Summary: {added_count} out of {len(cv_files)} resumes added successfully." |
| summary += f"\nπ Total resumes in database: {resume_db.get_resume_count()}" |
| |
| return "\n".join(results) + summary |
|
|
| def search_resumes_in_db(job_description, top_k=10): |
| """Search for matching resumes.""" |
| if not job_description.strip(): |
| return [], "β Please enter a job description." |
| |
| try: |
| results = resume_db.search_resumes(job_description, top_k=int(top_k)) |
| |
| if not results: |
| return [], f"β No matching resumes found. Add some resumes first! (Using: {DB_TYPE})" |
| |
| |
| display_data = [] |
| for resume_id, score, metadata in results: |
| display_data.append([ |
| metadata.get('filename', 'Unknown'), |
| f"{score:.4f}", |
| metadata.get('position_applied', 'N/A'), |
| metadata.get('upload_date', 'N/A')[:16] if metadata.get('upload_date') else 'N/A', |
| f"{metadata.get('text_length', 0)} chars" if 'text_length' in metadata else "N/A" |
| ]) |
| |
| status = f"β
Found {len(results)} matching resumes from {resume_db.get_resume_count()} total resumes. (Using: {DB_TYPE})" |
| return display_data, status |
| |
| except Exception as e: |
| logging.error(f"Search error: {e}") |
| return [], f"β Search failed: {str(e)}" |
|
|
| def get_database_stats(): |
| """Get current database statistics.""" |
| count = resume_db.get_resume_count() |
| return f"ποΈ Database Type: {DB_TYPE}\nπ Contains {count} resumes\nπ Pinecone Status: {pinecone_status}" |
|
|
| def clear_database(): |
| """Clear the database.""" |
| if resume_db.clear_database(): |
| return f"ποΈ Database cleared successfully! (Using: {DB_TYPE})" |
| else: |
| return "β Failed to clear database" |
|
|
| |
| |
| |
|
|
| def create_gradio_interface(): |
| with gr.Blocks(title="AI Resume Ranking System", theme=gr.themes.Soft()) as demo: |
| gr.Markdown(f""" |
| # π― AI Resume Ranking System |
| |
| **Upload resumes and find the best matches for your job openings!** |
| |
| Currently using: **{DB_TYPE}** |
| |
| This system uses TF-IDF vectorization and cosine similarity to rank resumes based on job descriptions. |
| """) |
| |
| with gr.Tab("π€ Upload Resumes"): |
| gr.Markdown("### Add New Resumes to Database") |
| |
| with gr.Row(): |
| with gr.Column(): |
| position_input = gr.Textbox( |
| label="Position Title (Optional)", |
| placeholder="e.g., Software Engineer, Data Scientist", |
| value="General" |
| ) |
| |
| upload_files = gr.File( |
| label="Upload Resumes (PDF/DOCX/TXT)", |
| file_count="multiple", |
| file_types=[".pdf", ".docx", ".txt"] |
| ) |
| |
| upload_btn = gr.Button("π Add to Database", variant="primary", size="lg") |
| |
| with gr.Column(): |
| upload_status = gr.Textbox( |
| label="Upload Status", |
| lines=12, |
| placeholder="Upload results will appear here..." |
| ) |
| |
| upload_btn.click( |
| fn=add_resumes_to_db, |
| inputs=[upload_files, position_input], |
| outputs=[upload_status] |
| ) |
| |
| with gr.Tab("π Search & Rank"): |
| gr.Markdown("### Find Best Resume Matches") |
| |
| with gr.Row(): |
| with gr.Column(): |
| job_description_input = gr.Textbox( |
| label="Job Description", |
| placeholder="Enter the job requirements, skills needed, and role description...", |
| lines=8 |
| ) |
| |
| with gr.Row(): |
| top_k_input = gr.Slider( |
| label="Number of Results", |
| minimum=1, |
| maximum=20, |
| value=10, |
| step=1 |
| ) |
| |
| search_btn = gr.Button("π Search & Rank", variant="primary", size="lg") |
| |
| search_results = gr.Dataframe( |
| headers=["Resume File", "Similarity Score", "Position", "Upload Date", "Size"], |
| label="π Ranking Results", |
| wrap=True |
| ) |
| |
| search_status = gr.Textbox(label="Search Status") |
| |
| search_btn.click( |
| fn=search_resumes_in_db, |
| inputs=[job_description_input, top_k_input], |
| outputs=[search_results, search_status] |
| ) |
| |
| with gr.Tab("π Database Info"): |
| gr.Markdown("### Database Management") |
| |
| with gr.Row(): |
| stats_btn = gr.Button("π Refresh Stats", variant="secondary") |
| clear_btn = gr.Button("ποΈ Clear Database", variant="stop") |
| |
| with gr.Row(): |
| stats_display = gr.Textbox(label="Database Statistics", lines=5) |
| clear_status = gr.Textbox(label="Clear Status", lines=3) |
| |
| stats_btn.click(fn=get_database_stats, outputs=[stats_display]) |
| clear_btn.click(fn=clear_database, outputs=[clear_status]) |
| |
| |
| demo.load(fn=get_database_stats, outputs=[stats_display]) |
| |
| with gr.Tab("π Setup"): |
| gr.Markdown(f""" |
| ### Current Configuration |
| |
| **Database Type**: {DB_TYPE} |
| **Pinecone Status**: {pinecone_status} |
| |
| ### To Enable Pinecone (Optional) |
| |
| If you want persistent storage with Pinecone: |
| |
| 1. **Get API Key**: Sign up at [pinecone.io](https://pinecone.io) and get your API key |
| 2. **Set Environment Variable**: Add `PINECONE_API_KEY` to your Hugging Face Space secrets |
| 3. **Restart Space**: The app will automatically detect and use Pinecone |
| |
| ### Hugging Face Spaces Setup |
| |
| 1. Go to your Space settings |
| 2. Click on "Variables and secrets" |
| 3. Add a new secret: |
| - Name: `PINECONE_API_KEY` |
| - Value: Your actual Pinecone API key |
| 4. Save and restart your Space |
| |
| ### Benefits of Pinecone |
| |
| - β
Persistent storage (data survives restarts) |
| - β
Scalable to millions of resumes |
| - β
Advanced vector search capabilities |
| - β
Real-time updates and deletions |
| """) |
| |
| with gr.Tab("βΉοΈ How It Works"): |
| gr.Markdown(""" |
| ### How This System Works |
| |
| 1. **Upload Resumes**: Add PDF, DOCX, or TXT resume files to the database |
| |
| 2. **Text Processing**: |
| - Extracts text from documents |
| - Removes common words (stopwords) |
| - Converts to lowercase and tokenizes |
| |
| 3. **Vectorization**: |
| - Uses TF-IDF (Term Frequency-Inverse Document Frequency) |
| - Creates numerical vectors representing document content |
| - Emphasizes rare but relevant terms |
| |
| 4. **Similarity Scoring**: |
| - Calculates cosine similarity between job description and resumes |
| - Ranks resumes by similarity score (0.0 to 1.0) |
| - Higher scores indicate better matches |
| |
| ### Database Options |
| |
| - **In-Memory**: Fast, but data is lost when session ends |
| - **Pinecone**: Persistent, scalable vector database (requires API key) |
| |
| ### Tips for Best Results |
| |
| - **Job Descriptions**: Be specific about required skills, technologies, and experience |
| - **Resume Quality**: Well-formatted resumes with clear text work best |
| - **Keywords**: Include industry-specific terms and technical skills |
| - **File Formats**: PDF and DOCX usually work better than scanned images |
| """) |
| |
| return demo |
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| app = create_gradio_interface() |
| app.launch() |