.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ resume_analyzer.db filter=lfs diff=lfs merge=lfs -text
agents/__pycache__/interview_scheduler.cpython-310.pyc ADDED
Binary file (2.96 kB). View file
 
agents/__pycache__/interview_scheduler.cpython-311.pyc ADDED
Binary file (4.29 kB). View file
 
agents/__pycache__/jd_summarizer.cpython-310.pyc ADDED
Binary file (639 Bytes). View file
 
agents/__pycache__/jd_summarizer.cpython-311.pyc ADDED
Binary file (849 Bytes). View file
 
agents/__pycache__/matcher.cpython-310.pyc ADDED
Binary file (3.06 kB). View file
 
agents/__pycache__/matcher.cpython-311.pyc ADDED
Binary file (5.22 kB). View file
 
agents/__pycache__/resume_extractor.cpython-310.pyc ADDED
Binary file (667 Bytes). View file
 
agents/__pycache__/resume_extractor.cpython-311.pyc ADDED
Binary file (871 Bytes). View file
 
agents/__pycache__/shortlister.cpython-310.pyc ADDED
Binary file (1 kB). View file
 
agents/__pycache__/shortlister.cpython-311.pyc ADDED
Binary file (1.46 kB). View file
 
agents/interview_scheduler.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/interview_scheduler.py
2
+
3
+ import datetime
4
+ from typing import Optional, Dict, Any
5
+
6
+ class InterviewScheduler:
7
+ def __init__(self, candidate_name: str):
8
+ self.candidate_name = candidate_name
9
+
10
+ def generate_interview_slots(self, start_date: datetime.datetime, days_ahead: int = 7) -> list:
11
+ """Generate available interview slots for the next week"""
12
+ slots = []
13
+ current_date = start_date
14
+
15
+ # Generate slots for the next week
16
+ for _ in range(days_ahead):
17
+ # Morning slots (9 AM to 12 PM)
18
+ for hour in range(9, 12):
19
+ slot = current_date.replace(hour=hour, minute=0)
20
+ slots.append(slot)
21
+
22
+ # Afternoon slots (2 PM to 5 PM)
23
+ for hour in range(14, 17):
24
+ slot = current_date.replace(hour=hour, minute=0)
25
+ slots.append(slot)
26
+
27
+ current_date += datetime.timedelta(days=1)
28
+
29
+ return slots
30
+
31
+ def generate_invite(self,
32
+ job_title: str,
33
+ interview_date: datetime.datetime,
34
+ interviewer: str,
35
+ meeting_link: Optional[str] = None,
36
+ additional_notes: Optional[str] = None) -> Dict[str, Any]:
37
+ """Generate a professional interview invitation"""
38
+
39
+ # Format the date and time
40
+ formatted_date = interview_date.strftime("%A, %B %d, %Y")
41
+ formatted_time = interview_date.strftime("%I:%M %p")
42
+
43
+ # Generate the invitation message
44
+ message = f"""
45
+ Dear {self.candidate_name},
46
+
47
+ Thank you for your interest in the {job_title} position. We are pleased to invite you for an interview.
48
+
49
+ Interview Details:
50
+ - Date: {formatted_date}
51
+ - Time: {formatted_time}
52
+ - Interviewer: {interviewer}
53
+ {f'- Meeting Link: {meeting_link}' if meeting_link else ''}
54
+
55
+ {f'Additional Notes: {additional_notes}' if additional_notes else ''}
56
+
57
+ Please confirm your availability for this interview slot. If this time doesn't work for you, please let us know your preferred time slots.
58
+
59
+ Best regards,
60
+ {interviewer}
61
+ """
62
+
63
+ return {
64
+ "candidate_name": self.candidate_name,
65
+ "job_title": job_title,
66
+ "interview_date": interview_date,
67
+ "interviewer": interviewer,
68
+ "meeting_link": meeting_link,
69
+ "additional_notes": additional_notes,
70
+ "message": message.strip(),
71
+ "status": "pending"
72
+ }
73
+
74
+ def generate_follow_up(self,
75
+ interview_date: datetime.datetime,
76
+ interviewer: str,
77
+ feedback: Optional[str] = None) -> Dict[str, Any]:
78
+ """Generate a follow-up message after the interview"""
79
+
80
+ message = f"""
81
+ Dear {self.candidate_name},
82
+
83
+ Thank you for taking the time to interview with us for the position. We appreciate your interest in joining our team.
84
+
85
+ {f'Interview Feedback: {feedback}' if feedback else 'We will review your interview and get back to you soon with next steps.'}
86
+
87
+ If you have any questions in the meantime, please don't hesitate to reach out.
88
+
89
+ Best regards,
90
+ {interviewer}
91
+ """
92
+
93
+ return {
94
+ "candidate_name": self.candidate_name,
95
+ "interview_date": interview_date,
96
+ "interviewer": interviewer,
97
+ "feedback": feedback,
98
+ "message": message.strip(),
99
+ "status": "follow_up"
100
+ }
agents/jd_summarizer.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # agents/jd_summarizer.py
2
+
3
+ class JobDescriptionSummarizer:
4
+ def __init__(self, job_description: str):
5
+ self.job_description = job_description
6
+
7
+ def get_summary(self):
8
+ # Placeholder: In future, use LLM to generate a true summary
9
+ return self.job_description.strip()
agents/matcher.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from models.llm_client import LLMClient
3
+
4
+ class ResumeJDMatcher:
5
+ def __init__(self, llm: LLMClient):
6
+ self.llm = llm
7
+
8
+ def match_resume_to_job(self, resume_text, job_description):
9
+ if not resume_text or not job_description:
10
+ raise ValueError("Both resume text and job description are required")
11
+
12
+ prompt = f"""You are a resume matching assistant. Your task is to analyze how well a resume matches a job description and return a JSON response.
13
+
14
+ IMPORTANT: You must ONLY return a JSON object. No other text, explanations, or code blocks.
15
+
16
+ Resume to analyze:
17
+ {resume_text}
18
+
19
+ Job Description:
20
+ {job_description}
21
+
22
+ Return a single JSON object with these exact fields:
23
+ {{
24
+ "skills_match": (number 0-100),
25
+ "experience_match": (number 0-100),
26
+ "education_match": (number 0-100),
27
+ "certifications_match": (number 0-100),
28
+ "summary": "Brief analysis of the match"
29
+ }}"""
30
+
31
+ try:
32
+ response = self.llm.generate_text(
33
+ prompt,
34
+ temperature=0.3
35
+ )
36
+
37
+ # Log raw response for debugging
38
+ print(f"[DEBUG] Raw response: {response}")
39
+ with open("llm_raw_output.txt", "w") as f:
40
+ f.write(response)
41
+
42
+ parsed = self._clean_and_parse_response(response)
43
+
44
+ required_fields = ["skills_match", "experience_match", "education_match", "certifications_match", "summary"]
45
+ for field in required_fields:
46
+ if field not in parsed:
47
+ raise ValueError(f"Missing required field: {field}")
48
+ if field != "summary":
49
+ if not isinstance(parsed[field], (int, float)):
50
+ raise ValueError(f"Invalid value type for {field}: {parsed[field]}")
51
+ if parsed[field] < 0 or parsed[field] > 100:
52
+ raise ValueError(f"Score out of range for {field}: {parsed[field]}")
53
+
54
+ return parsed
55
+
56
+ except Exception as e:
57
+ print(f"[ERROR] Matching failed: {str(e)}")
58
+ return {
59
+ "skills_match": 0,
60
+ "experience_match": 0,
61
+ "education_match": 0,
62
+ "certifications_match": 0,
63
+ "summary": f"Error during matching: {str(e)}"
64
+ }
65
+
66
+ def _clean_and_parse_response(self, response: str) -> dict:
67
+ """Clean and parse the LLM response into a JSON object."""
68
+ try:
69
+ response = response.strip()
70
+
71
+ # Remove known prefixes that LLM might include
72
+ for prefix in ["Example:", "Response:", "Answer:", "Here's the JSON:"]:
73
+ if response.startswith(prefix):
74
+ response = response[len(prefix):].strip()
75
+
76
+ # Find the JSON object boundaries
77
+ start_idx = response.find('{')
78
+ end_idx = response.rfind('}')
79
+
80
+ if start_idx == -1 or end_idx == -1:
81
+ raise ValueError("No JSON object found in response")
82
+
83
+ json_str = response[start_idx:end_idx + 1].strip()
84
+
85
+ # Log the sanitized string before parsing
86
+ print("[DEBUG] JSON to parse:", json_str)
87
+
88
+ # Parse into dict
89
+ result = json.loads(json_str)
90
+
91
+ # Convert numeric fields safely
92
+ for key in ["skills_match", "experience_match", "education_match", "certifications_match"]:
93
+ if key in result:
94
+ try:
95
+ result[key] = float(result[key])
96
+ except (ValueError, TypeError):
97
+ raise ValueError(f"Invalid numeric value for {key}: {result[key]}")
98
+
99
+ return result
100
+
101
+ except Exception as e:
102
+ raise ValueError(f"Failed to parse response: {str(e)}")
agents/resume_extractor.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/resume_extractor.py
2
+
3
+ from utils.pdf_utils import extract_text_from_pdf
4
+
5
+ class ResumeExtractor:
6
+ def __init__(self, uploaded_file):
7
+ self.uploaded_file = uploaded_file
8
+
9
+ def get_resume_text(self):
10
+ return extract_text_from_pdf(self.uploaded_file)
agents/shortlister.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/shortlister.py
2
+
3
+ import re
4
+
5
+ class Shortlister:
6
+ def __init__(self, threshold=70):
7
+ self.threshold = threshold
8
+
9
+ def compute_final_score(self, scores):
10
+ return round(
11
+ 0.4 * scores.get("skills_match", 0) +
12
+ 0.3 * scores.get("experience_match", 0) +
13
+ 0.2 * scores.get("education_match", 0) +
14
+ 0.1 * scores.get("certifications_match", 0),
15
+ 2
16
+ )
17
+
18
+ def is_shortlisted(self, final_score, threshold=60.0):
19
+ return final_score >= threshold
20
+
db/__pycache__/database.cpython-310.pyc ADDED
Binary file (7.78 kB). View file
 
db/__pycache__/database.cpython-311.pyc ADDED
Binary file (11.3 kB). View file
 
db/database.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # db/database.py
2
+
3
+ import sqlite3
4
+ from datetime import datetime
5
+ import json
6
+
7
+ class ResumeMatchDB:
8
+ def __init__(self, db_path="resume_analyzer.db"):
9
+ self.db_path = db_path
10
+ self._init_db()
11
+
12
+ def _init_db(self):
13
+ """Initialize database tables"""
14
+ with sqlite3.connect(self.db_path) as conn:
15
+ cursor = conn.cursor()
16
+
17
+ # Candidates table
18
+ cursor.execute('''
19
+ CREATE TABLE IF NOT EXISTS candidates (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ name TEXT NOT NULL,
22
+ email TEXT,
23
+ resume_path TEXT NOT NULL,
24
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
25
+ )
26
+ ''')
27
+
28
+ # Job Descriptions table
29
+ cursor.execute('''
30
+ CREATE TABLE IF NOT EXISTS job_descriptions (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ title TEXT NOT NULL,
33
+ description TEXT NOT NULL,
34
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
35
+ )
36
+ ''')
37
+
38
+ # Matches table
39
+ cursor.execute('''
40
+ CREATE TABLE IF NOT EXISTS matches (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ candidate_id INTEGER,
43
+ job_id INTEGER,
44
+ match_score REAL,
45
+ skills_match REAL,
46
+ experience_match REAL,
47
+ education_match REAL,
48
+ certifications_match REAL,
49
+ summary TEXT,
50
+ is_shortlisted BOOLEAN,
51
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
52
+ FOREIGN KEY (candidate_id) REFERENCES candidates (id),
53
+ FOREIGN KEY (job_id) REFERENCES job_descriptions (id)
54
+ )
55
+ ''')
56
+
57
+ # Interviews table
58
+ cursor.execute('''
59
+ CREATE TABLE IF NOT EXISTS interviews (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ candidate_id INTEGER,
62
+ job_id INTEGER,
63
+ scheduled_date TIMESTAMP,
64
+ status TEXT DEFAULT 'pending',
65
+ interviewer TEXT,
66
+ meeting_link TEXT,
67
+ notes TEXT,
68
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
69
+ FOREIGN KEY (candidate_id) REFERENCES candidates (id),
70
+ FOREIGN KEY (job_id) REFERENCES job_descriptions (id)
71
+ )
72
+ ''')
73
+
74
+ conn.commit()
75
+
76
+ def insert_candidate(self, name, email, resume_path):
77
+ """Insert a new candidate"""
78
+ with sqlite3.connect(self.db_path) as conn:
79
+ cursor = conn.cursor()
80
+ cursor.execute(
81
+ "INSERT INTO candidates (name, email, resume_path) VALUES (?, ?, ?)",
82
+ (name, email, resume_path)
83
+ )
84
+ return cursor.lastrowid
85
+
86
+ def insert_job_description(self, title, description):
87
+ """Insert a new job description"""
88
+ with sqlite3.connect(self.db_path) as conn:
89
+ cursor = conn.cursor()
90
+ cursor.execute(
91
+ "INSERT INTO job_descriptions (title, description) VALUES (?, ?)",
92
+ (title, description)
93
+ )
94
+ return cursor.lastrowid
95
+
96
+ def insert_match_result(self, candidate_id, job_id, match_data):
97
+ """Insert match results"""
98
+ with sqlite3.connect(self.db_path) as conn:
99
+ cursor = conn.cursor()
100
+ cursor.execute('''
101
+ INSERT INTO matches (
102
+ candidate_id, job_id, match_score, skills_match,
103
+ experience_match, education_match, certifications_match,
104
+ summary, is_shortlisted
105
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
106
+ ''', (
107
+ candidate_id, job_id,
108
+ match_data['match_score'],
109
+ match_data['skills_match'],
110
+ match_data['experience_match'],
111
+ match_data['education_match'],
112
+ match_data['certifications_match'],
113
+ match_data['summary'],
114
+ match_data['is_shortlisted']
115
+ ))
116
+ return cursor.lastrowid
117
+
118
+ def schedule_interview(self, candidate_id, job_id, scheduled_date, interviewer, meeting_link=None, notes=None):
119
+ """Schedule an interview"""
120
+ with sqlite3.connect(self.db_path) as conn:
121
+ cursor = conn.cursor()
122
+ cursor.execute('''
123
+ INSERT INTO interviews (
124
+ candidate_id, job_id, scheduled_date,
125
+ interviewer, meeting_link, notes
126
+ ) VALUES (?, ?, ?, ?, ?, ?)
127
+ ''', (candidate_id, job_id, scheduled_date, interviewer, meeting_link, notes))
128
+ return cursor.lastrowid
129
+
130
+ def get_candidate_matches(self, candidate_id):
131
+ """Get all matches for a candidate"""
132
+ with sqlite3.connect(self.db_path) as conn:
133
+ conn.row_factory = sqlite3.Row
134
+ cursor = conn.cursor()
135
+ cursor.execute('''
136
+ SELECT m.*, j.title as job_title, j.description as job_description
137
+ FROM matches m
138
+ JOIN job_descriptions j ON m.job_id = j.id
139
+ WHERE m.candidate_id = ?
140
+ ORDER BY m.match_score DESC
141
+ ''', (candidate_id,))
142
+ return [dict(row) for row in cursor.fetchall()]
143
+
144
+ def get_scheduled_interviews(self, status=None):
145
+ """Get all scheduled interviews, optionally filtered by status"""
146
+ with sqlite3.connect(self.db_path) as conn:
147
+ conn.row_factory = sqlite3.Row # This makes the cursor return dictionaries
148
+ cursor = conn.cursor()
149
+ if status:
150
+ cursor.execute('''
151
+ SELECT i.*, c.name as candidate_name, c.email as candidate_email,
152
+ j.title as job_title
153
+ FROM interviews i
154
+ JOIN candidates c ON i.candidate_id = c.id
155
+ JOIN job_descriptions j ON i.job_id = j.id
156
+ WHERE i.status = ?
157
+ ORDER BY i.scheduled_date
158
+ ''', (status,))
159
+ else:
160
+ cursor.execute('''
161
+ SELECT i.*, c.name as candidate_name, c.email as candidate_email,
162
+ j.title as job_title
163
+ FROM interviews i
164
+ JOIN candidates c ON i.candidate_id = c.id
165
+ JOIN job_descriptions j ON i.job_id = j.id
166
+ ORDER BY i.scheduled_date
167
+ ''')
168
+ return [dict(row) for row in cursor.fetchall()]
169
+
170
+ def update_interview_status(self, interview_id, status, notes=None):
171
+ """Update interview status and notes"""
172
+ with sqlite3.connect(self.db_path) as conn:
173
+ cursor = conn.cursor()
174
+ if notes:
175
+ cursor.execute('''
176
+ UPDATE interviews
177
+ SET status = ?, notes = ?
178
+ WHERE id = ?
179
+ ''', (status, notes, interview_id))
180
+ else:
181
+ cursor.execute('''
182
+ UPDATE interviews
183
+ SET status = ?
184
+ WHERE id = ?
185
+ ''', (status, interview_id))
186
+ conn.commit()
187
+
188
+ def close(self):
189
+ """Close database connection"""
190
+ pass # SQLite connections are automatically closed
llm_raw_output.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "skills_match": 100,
3
+ "experience_match": 100,
4
+ "education_match": 100,
5
+ "certifications_match": 100,
6
+ "summary": "Perfect match: Resume directly aligns with job requirements including specific technologies (Python, FastAPI, RAG, FAISS, DeepSeek, Llama), cloud deployment (GCP), and experience building AI automation systems and real-time intelligence layers."
7
+ }
main.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import PyPDF2
6
+ import json
7
+ import os
8
+ import re
9
+ import datetime
10
+ from dotenv import load_dotenv
11
+ from models.llm_client import LLMClient
12
+ from agents.resume_extractor import ResumeExtractor
13
+ from agents.jd_summarizer import JobDescriptionSummarizer
14
+ from agents.matcher import ResumeJDMatcher
15
+ from agents.shortlister import Shortlister
16
+ from agents.interview_scheduler import InterviewScheduler
17
+ from db.database import ResumeMatchDB
18
+
19
+ # Load environment variables
20
+ load_dotenv()
21
+
22
+ # === Streamlit UI ===
23
+ st.set_page_config(page_title="AI Resume Analyzer", page_icon="📋", layout="wide")
24
+ st.title("📋 AI Resume Analyzer")
25
+
26
+ # Initialize session state
27
+ if 'results' not in st.session_state:
28
+ st.session_state.results = []
29
+ if 'scheduled_interviews' not in st.session_state:
30
+ st.session_state.scheduled_interviews = []
31
+ if 'interview_data' not in st.session_state:
32
+ st.session_state.interview_data = {}
33
+ if 'search_query' not in st.session_state:
34
+ st.session_state.search_query = ""
35
+
36
+ # === LLM Configuration (DeepSeek) ===
37
+ DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
38
+ DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
39
+ DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
40
+
41
+ if not DEEPSEEK_API_KEY:
42
+ st.error("❌ DEEPSEEK_API_KEY not found. Please set it in your .env file.")
43
+ st.stop()
44
+
45
+ # === Upload Resume ===
46
+ st.subheader("📂 Upload Resumes")
47
+ uploaded_files = st.file_uploader("Upload multiple resumes (PDF)", type=["pdf"], accept_multiple_files=True)
48
+
49
+ # === Job Description Input ===
50
+ st.subheader("📝 Job Description")
51
+ jd_input_type = st.radio("Select input method:", ["Text Input", "Upload File"], horizontal=True)
52
+ job_descriptions = []
53
+
54
+ if jd_input_type == "Text Input":
55
+ job_description = st.text_area("Enter job description:", height=200)
56
+ if job_description:
57
+ job_descriptions.append({"title": "Job Description", "content": job_description})
58
+
59
+ elif jd_input_type == "Upload File":
60
+ jd_file = st.file_uploader("Upload job description file (PDF, TXT, or CSV)", type=["pdf", "txt", "csv"])
61
+
62
+ if jd_file:
63
+ if jd_file.type == "application/pdf":
64
+ reader = PyPDF2.PdfReader(jd_file)
65
+ content = " ".join(page.extract_text() for page in reader.pages)
66
+ job_descriptions.append({"title": jd_file.name, "content": content})
67
+
68
+ elif jd_file.type == "text/plain":
69
+ content = jd_file.read().decode("utf-8")
70
+ job_descriptions.append({"title": jd_file.name, "content": content})
71
+
72
+ elif jd_file.type == "text/csv":
73
+ try:
74
+ encodings = ['utf-8', 'cp1252', 'latin1', 'iso-8859-1']
75
+ df = None
76
+
77
+ for encoding in encodings:
78
+ try:
79
+ jd_file.seek(0)
80
+ df = pd.read_csv(jd_file, encoding=encoding)
81
+ break
82
+ except UnicodeDecodeError:
83
+ continue
84
+
85
+ if df is None:
86
+ st.error("❌ Could not read CSV file with any supported encoding")
87
+ elif 'Job Title' in df.columns and 'Job Description' in df.columns:
88
+ job_descriptions = [{"title": row['Job Title'], "content": row['Job Description']}
89
+ for _, row in df.iterrows()]
90
+ else:
91
+ st.error("❌ CSV must contain 'Job Title' and 'Job Description' columns")
92
+ except Exception as e:
93
+ st.error(f"❌ Error reading CSV file: {str(e)}")
94
+
95
+ # Database status indicator
96
+ try:
97
+ db = ResumeMatchDB()
98
+ st.sidebar.success("✅ Database Connected")
99
+ except Exception as e:
100
+ st.sidebar.error("❌ Database Connection Error")
101
+
102
+ # Search functionality
103
+ st.sidebar.subheader("🔍 Search Candidates")
104
+
105
+ # ✅ Initialize session state key if not already present
106
+ if "search_query" not in st.session_state:
107
+ st.session_state.search_query = ""
108
+
109
+ # 🔍 Display search input
110
+ st.session_state.search_query = st.sidebar.text_input("Search by name or role", st.session_state.search_query)
111
+
112
+ def format_date(date_str):
113
+ """Format date string for better readability"""
114
+ try:
115
+ date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
116
+ return date.strftime("%B %d, %Y at %I:%M %p")
117
+ except:
118
+ return date_str
119
+
120
+ def display_statistics():
121
+ """Display basic statistics"""
122
+ if st.session_state.results:
123
+ total_candidates = len(st.session_state.results)
124
+ shortlisted = sum(1 for r in st.session_state.results if r['best_match']['is_shortlisted'])
125
+
126
+ col1, col2 = st.columns(2)
127
+ with col1:
128
+ st.metric("Total Candidates", total_candidates)
129
+ with col2:
130
+ st.metric("Shortlisted", shortlisted)
131
+
132
+ def filter_results(results, query):
133
+ """Filter results based on search query"""
134
+ if not query:
135
+ return results
136
+ query = query.lower()
137
+ return [
138
+ r for r in results
139
+ if query in r['candidate_name'].lower() or
140
+ query in r['best_match']['job_title'].lower()
141
+ ]
142
+
143
+ def extract_candidate_info(resume_text):
144
+ """Extract candidate name and email from resume text"""
145
+ email_pattern = r'[\w\.-]+@[\w\.-]+\.\w+'
146
+ email = re.search(email_pattern, resume_text)
147
+ email = email.group(0) if email else "Not found"
148
+
149
+ lines = resume_text.split('\n')
150
+ name = "Not found"
151
+
152
+ name_patterns = [
153
+ r'^[A-Z][a-z]+\s+[A-Z][a-z]+$',
154
+ r'^[A-Z][a-z]+\s+[A-Z]\.\s+[A-Z][a-z]+$',
155
+ r'^[A-Z][a-z]+\s+[A-Z][a-z]+\s+[A-Z][a-z]+$'
156
+ ]
157
+
158
+ for line in lines:
159
+ line = line.strip()
160
+ if line and '@' not in line:
161
+ for pattern in name_patterns:
162
+ if re.match(pattern, line):
163
+ name = line
164
+ break
165
+ if name != "Not found":
166
+ break
167
+
168
+ return name, email
169
+
170
+ def analyze_resumes():
171
+ """Analyze resumes and store results in session state"""
172
+ progress_bar = st.progress(0)
173
+ status_text = st.empty()
174
+
175
+ with st.spinner("Analyzing resumes..."):
176
+ results = []
177
+ total_steps = len(uploaded_files) * len(job_descriptions)
178
+ current_step = 0
179
+
180
+ db = ResumeMatchDB()
181
+
182
+ for uploaded_file in uploaded_files:
183
+ status_text.text(f"📄 Processing {uploaded_file.name}...")
184
+ extractor = ResumeExtractor(uploaded_file)
185
+ resume_text = extractor.get_resume_text()
186
+ candidate_name, candidate_email = extract_candidate_info(resume_text)
187
+
188
+ candidate_id = db.insert_candidate(
189
+ name=candidate_name,
190
+ email=candidate_email,
191
+ resume_path=uploaded_file.name
192
+ )
193
+
194
+ resume_results = []
195
+ for jd in job_descriptions:
196
+ current_step += 1
197
+ progress = current_step / total_steps
198
+ progress_bar.progress(progress)
199
+
200
+ status_text.text(f"🔍 Matching with {jd['title']}...")
201
+ jd_agent = JobDescriptionSummarizer(jd['content'])
202
+ jd_summary = jd_agent.get_summary()
203
+
204
+ llm = LLMClient(api_key=DEEPSEEK_API_KEY, model_name=DEEPSEEK_MODEL, base_url=DEEPSEEK_BASE_URL)
205
+ matcher = ResumeJDMatcher(llm)
206
+ shortlister = Shortlister(threshold=70.0)
207
+
208
+ match_result = matcher.match_resume_to_job(resume_text, jd_summary)
209
+ match_percent = shortlister.compute_final_score(match_result)
210
+ is_shortlisted = shortlister.is_shortlisted(match_percent)
211
+
212
+ job_id = db.insert_job_description(
213
+ title=jd['title'],
214
+ description=jd['content']
215
+ )
216
+
217
+ match_data = {
218
+ 'match_score': match_percent,
219
+ 'skills_match': match_result['skills_match'],
220
+ 'experience_match': match_result['experience_match'],
221
+ 'education_match': match_result['education_match'],
222
+ 'certifications_match': match_result['certifications_match'],
223
+ 'summary': match_result['summary'],
224
+ 'is_shortlisted': is_shortlisted
225
+ }
226
+ db.insert_match_result(candidate_id, job_id, match_data)
227
+
228
+ resume_results.append({
229
+ "job_title": jd['title'],
230
+ "match_score": match_percent,
231
+ "is_shortlisted": is_shortlisted,
232
+ "details": match_result,
233
+ "job_id": job_id
234
+ })
235
+
236
+ best_match = max(resume_results, key=lambda x: x['match_score'])
237
+
238
+ results.append({
239
+ "candidate_name": candidate_name,
240
+ "candidate_email": candidate_email,
241
+ "resume_name": uploaded_file.name,
242
+ "best_match": best_match,
243
+ "candidate_id": candidate_id
244
+ })
245
+
246
+ progress_bar.empty()
247
+ status_text.empty()
248
+ st.session_state.results = results
249
+
250
+ def display_results():
251
+ """Display analysis results and handle interview scheduling"""
252
+ if not st.session_state.results:
253
+ return
254
+
255
+ # Display statistics
256
+ display_statistics()
257
+
258
+ st.subheader("🎯 Analysis Results")
259
+
260
+ # Filter results based on search
261
+ filtered_results = filter_results(st.session_state.results, st.session_state.search_query)
262
+
263
+ # Create a list to store all candidates for download
264
+ all_candidates = []
265
+
266
+ for result in filtered_results:
267
+ with st.expander(f"📄 {result['candidate_name']} ({result['candidate_email']})"):
268
+ st.write(f"**Resume:** {result['resume_name']}")
269
+
270
+ match = result['best_match']
271
+ st.subheader(f"Best Match: {match['job_title']}")
272
+ st.metric("Match Score", f"{match['match_score']:.1f}%")
273
+
274
+ all_candidates.append({
275
+ "Name": result['candidate_name'],
276
+ "Email": result['candidate_email'],
277
+ "Resume": result['resume_name'],
278
+ "Best Match Role": match['job_title'],
279
+ "Match Score": f"{match['match_score']:.1f}%",
280
+ "Status": "Shortlisted" if match['is_shortlisted'] else "Not Shortlisted"
281
+ })
282
+
283
+ if match['is_shortlisted']:
284
+ st.success("✅ Shortlisted")
285
+ handle_interview_scheduling(result, match)
286
+ else:
287
+ st.warning("⚠️ Not Shortlisted")
288
+
289
+ display_match_details(match['details'])
290
+
291
+ # Add download button for candidate list
292
+ if all_candidates:
293
+ df_candidates = pd.DataFrame(all_candidates)
294
+ csv = df_candidates.to_csv(index=False).encode('utf-8')
295
+ st.download_button(
296
+ label="📥 Download Candidate List",
297
+ data=csv,
298
+ file_name="candidate_list.csv",
299
+ mime="text/csv"
300
+ )
301
+
302
+ def handle_interview_scheduling(result, match):
303
+ """Handle interview scheduling for a candidate"""
304
+ st.subheader("📅 Schedule Interview")
305
+
306
+ interview_key = f"interview_{result['candidate_id']}"
307
+ if interview_key not in st.session_state.interview_data:
308
+ st.session_state.interview_data[interview_key] = {
309
+ "interviewer": "",
310
+ "meeting_link": "",
311
+ "notes": "",
312
+ "selected_slot": None
313
+ }
314
+
315
+ data = st.session_state.interview_data[interview_key]
316
+
317
+ data["interviewer"] = st.text_input(
318
+ "Interviewer Name",
319
+ value=data["interviewer"],
320
+ key=f"interviewer_{result['candidate_id']}"
321
+ )
322
+
323
+ data["meeting_link"] = st.text_input(
324
+ "Meeting Link (optional)",
325
+ value=data["meeting_link"],
326
+ key=f"meeting_{result['candidate_id']}"
327
+ )
328
+
329
+ data["notes"] = st.text_area(
330
+ "Additional Notes",
331
+ value=data["notes"],
332
+ key=f"notes_{result['candidate_id']}"
333
+ )
334
+
335
+ scheduler = InterviewScheduler(result['candidate_name'])
336
+ start_date = datetime.datetime.now() + datetime.timedelta(days=1)
337
+ slots = scheduler.generate_interview_slots(start_date)
338
+
339
+ data["selected_slot"] = st.selectbox(
340
+ "Select Interview Slot",
341
+ options=slots,
342
+ format_func=lambda x: x.strftime("%A, %B %d, %Y at %I:%M %p"),
343
+ key=f"slot_{result['candidate_id']}",
344
+ index=slots.index(data["selected_slot"]) if data["selected_slot"] in slots else 0
345
+ )
346
+
347
+ if st.button("Schedule Interview", key=f"schedule_{result['candidate_id']}"):
348
+ if data["selected_slot"] and data["interviewer"]:
349
+ schedule_interview(result, match, data)
350
+ else:
351
+ st.error("Please provide interviewer name and select a time slot")
352
+
353
+ def schedule_interview(result, match, data):
354
+ """Schedule an interview and update session state"""
355
+ db = ResumeMatchDB()
356
+ scheduler = InterviewScheduler(result['candidate_name'])
357
+
358
+ invite = scheduler.generate_invite(
359
+ job_title=match['job_title'],
360
+ interview_date=data["selected_slot"],
361
+ interviewer=data["interviewer"],
362
+ meeting_link=data["meeting_link"],
363
+ additional_notes=data["notes"]
364
+ )
365
+
366
+ db.schedule_interview(
367
+ candidate_id=result['candidate_id'],
368
+ job_id=match['job_id'],
369
+ scheduled_date=data["selected_slot"],
370
+ interviewer=data["interviewer"],
371
+ meeting_link=data["meeting_link"],
372
+ notes=data["notes"]
373
+ )
374
+
375
+ st.session_state.scheduled_interviews.append({
376
+ "candidate_id": result['candidate_id'],
377
+ "candidate_name": result['candidate_name'],
378
+ "job_title": match['job_title'],
379
+ "interview_date": data["selected_slot"],
380
+ "interviewer": data["interviewer"]
381
+ })
382
+
383
+ st.success("✅ Interview Scheduled!")
384
+ st.write("**Interview Invitation:**")
385
+ st.write(invite['message'])
386
+
387
+ def display_match_details(details):
388
+ """Display match details in a clean format"""
389
+ col1, col2 = st.columns(2)
390
+ with col1:
391
+ st.metric("Skills Match", f"{details['skills_match']}%")
392
+ st.metric("Experience Match", f"{details['experience_match']}%")
393
+ with col2:
394
+ st.metric("Education Match", f"{details['education_match']}%")
395
+ st.metric("Certifications Match", f"{details['certifications_match']}%")
396
+
397
+ st.write("**Summary:**")
398
+ st.write(details['summary'])
399
+
400
+ def display_scheduled_interviews():
401
+ """Display scheduled interviews and handle feedback"""
402
+ st.subheader("📅 Upcoming Interviews")
403
+ db = ResumeMatchDB()
404
+ interviews = db.get_scheduled_interviews(status='pending')
405
+
406
+ if interviews:
407
+ for interview in interviews:
408
+ with st.expander(f"Interview with {interview['candidate_name']} for {interview['job_title']}"):
409
+ st.write(f"**Date:** {format_date(interview['scheduled_date'])}")
410
+ st.write(f"**Interviewer:** {interview['interviewer']}")
411
+ if interview['meeting_link']:
412
+ st.write(f"**Meeting Link:** {interview['meeting_link']}")
413
+ if interview['notes']:
414
+ st.write(f"**Notes:** {interview['notes']}")
415
+
416
+ feedback = st.text_area("Interview Feedback", key=f"feedback_{interview['id']}")
417
+ if st.button("Submit Feedback", key=f"submit_{interview['id']}"):
418
+ if feedback:
419
+ db.update_interview_status(
420
+ interview_id=interview['id'],
421
+ status='completed',
422
+ notes=feedback
423
+ )
424
+ st.success("✅ Feedback submitted!")
425
+ else:
426
+ st.error("Please provide feedback")
427
+ else:
428
+ st.info("No upcoming interviews scheduled.")
429
+
430
+ # Main execution
431
+ if uploaded_files and job_descriptions:
432
+ if st.button("🔍 Analyze Resumes"):
433
+ analyze_resumes()
434
+
435
+ if st.session_state.results:
436
+ display_results()
437
+ display_scheduled_interviews()
models/__pycache__/llm_client.cpython-310.pyc ADDED
Binary file (1.57 kB). View file
 
models/__pycache__/llm_client.cpython-311.pyc ADDED
Binary file (2.65 kB). View file
 
models/llm_client.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models/llm_client.py
2
+
3
+ from openai import OpenAI
4
+
5
+
6
+ class LLMClient:
7
+ def __init__(self, api_key: str, model_name: str = "deepseek-chat", base_url: str = "https://api.deepseek.com/v1"):
8
+ if not api_key:
9
+ raise ValueError("API key is required")
10
+
11
+ # Initialize OpenAI-compatible client for DeepSeek
12
+ self.client = OpenAI(api_key=api_key, base_url=base_url)
13
+ self.model_name = model_name
14
+
15
+ def generate_text(self, prompt: str, max_tokens: int = 300, temperature: float = 0.7) -> str:
16
+ """Generate text using DeepSeek Chat (OpenAI-compatible API).
17
+
18
+ Args:
19
+ prompt: The input prompt
20
+ max_tokens: Maximum number of tokens to generate
21
+ temperature: Temperature for sampling (higher = more random)
22
+
23
+ Returns:
24
+ str: The generated text response
25
+ """
26
+ try:
27
+ print(f"[DEBUG] Prompt sent to LLM: {prompt[:200]}...")
28
+
29
+ completion = self.client.chat.completions.create(
30
+ model=self.model_name,
31
+ messages=[
32
+ {"role": "system", "content": "You are a helpful assistant for resume and job description analysis."},
33
+ {"role": "user", "content": prompt},
34
+ ],
35
+ max_tokens=max_tokens,
36
+ temperature=temperature,
37
+ )
38
+
39
+ if not completion or not completion.choices:
40
+ raise ValueError("Empty response from LLM API")
41
+
42
+ generated_text = (completion.choices[0].message.content or "").strip()
43
+
44
+ if not generated_text:
45
+ raise ValueError("Empty response from LLM API")
46
+
47
+ return generated_text
48
+
49
+ except Exception as e:
50
+ raise RuntimeError(f"LLM API error: {str(e)}")
resume_analyzer.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b7ddad4a7c1a139712c3d2a774a4a4b15f56d035261a041b2cf89bacb0655c5
3
+ size 5169152
resume_matches.db ADDED
Binary file (28.7 kB). View file
 
utils/__pycache__/pdf_utils.cpython-310.pyc ADDED
Binary file (662 Bytes). View file
 
utils/__pycache__/pdf_utils.cpython-311.pyc ADDED
Binary file (1.11 kB). View file
 
utils/pdf_utils.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/pdf_utils.py
2
+
3
+ import PyPDF2
4
+
5
+ def extract_text_from_pdf(uploaded_file):
6
+ try:
7
+ reader = PyPDF2.PdfReader(uploaded_file)
8
+ resume_text = " ".join(
9
+ [page.extract_text() for page in reader.pages if page.extract_text()]
10
+ )
11
+ return resume_text.strip()
12
+ except Exception as e:
13
+ raise RuntimeError(f"Failed to extract PDF text: {e}")