forked from interviewstreet/hiring-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscore.py
More file actions
377 lines (317 loc) · 12.8 KB
/
Copy pathscore.py
File metadata and controls
377 lines (317 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
import os
import sys
import json
import logging
import csv
from pdf import PDFHandler
from github import fetch_and_display_github_info
from models import JSONResume, EvaluationData
from typing import List, Optional, Dict
from evaluator import ResumeEvaluator
from pathlib import Path
from prompt import DEFAULT_MODEL, MODEL_PARAMETERS
from transform import (
transform_evaluation_response,
convert_json_resume_to_text,
convert_github_data_to_text,
convert_blog_data_to_text,
)
from config import DEVELOPMENT_MODE
logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)5s - %(lineno)5d - %(funcName)33s - %(levelname)5s - %(message)s",
)
def print_evaluation_results(
evaluation: EvaluationData, candidate_name: str = "Candidate"
):
"""Print evaluation results in a readable format."""
print("\n" + "=" * 80)
print(f"📊 RESUME EVALUATION RESULTS FOR: {candidate_name}")
print("=" * 80)
if not evaluation:
print("❌ No evaluation data available")
return
# Calculate overall score
total_score = 0
max_score = 0
if hasattr(evaluation, "scores") and evaluation.scores:
for category_name, category_data in evaluation.scores.model_dump().items():
category_score = min(category_data["score"], category_data["max"])
total_score += category_score
max_score += category_data["max"]
# Log warning if score was capped
if category_score < category_data["score"]:
print(
f"⚠️ Warning: {category_name} score capped from {category_data['score']} to {category_score} (max: {category_data['max']})"
)
# Add bonus points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
total_score += evaluation.bonus_points.total
# Subtract deductions
if hasattr(evaluation, "deductions") and evaluation.deductions:
total_score -= evaluation.deductions.total
# Ensure total score doesn't exceed maximum possible score
max_possible_score = max_score + 20 # 120 (100 categories + 20 bonus)
if total_score > max_possible_score:
total_score = max_possible_score
print(f"⚠️ Warning: Total score capped at maximum possible value")
# Overall Score
print(f"\n🎯 OVERALL SCORE: {total_score:.1f}/{max_score}")
# Detailed Scores
print("\n📈 DETAILED SCORES:")
print("-" * 60)
if hasattr(evaluation, "scores") and evaluation.scores:
# Define category maximums
category_maxes = {
"open_source": 35,
"self_projects": 30,
"production": 25,
"technical_skills": 10,
}
# Open Source
if hasattr(evaluation.scores, "open_source") and evaluation.scores.open_source:
os_score = evaluation.scores.open_source
capped_score = min(os_score.score, category_maxes["open_source"])
print(f"🌐 Open Source: {capped_score}/{os_score.max}")
print(f" Evidence: {os_score.evidence}")
print()
# Self Projects
if (
hasattr(evaluation.scores, "self_projects")
and evaluation.scores.self_projects
):
sp_score = evaluation.scores.self_projects
capped_score = min(sp_score.score, category_maxes["self_projects"])
print(f"🚀 Self Projects: {capped_score}/{sp_score.max}")
print(f" Evidence: {sp_score.evidence}")
print()
# Production Experience
if hasattr(evaluation.scores, "production") and evaluation.scores.production:
prod_score = evaluation.scores.production
capped_score = min(prod_score.score, category_maxes["production"])
print(f"🏢 Production Experience: {capped_score}/{prod_score.max}")
print(f" Evidence: {prod_score.evidence}")
print()
# Technical Skills
if (
hasattr(evaluation.scores, "technical_skills")
and evaluation.scores.technical_skills
):
tech_score = evaluation.scores.technical_skills
capped_score = min(tech_score.score, category_maxes["technical_skills"])
print(f"💻 Technical Skills: {capped_score}/{tech_score.max}")
print(f" Evidence: {tech_score.evidence}")
print()
# Bonus Points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
print(f"\n⭐ BONUS POINTS: {evaluation.bonus_points.total}")
print("-" * 30)
print(f" {evaluation.bonus_points.breakdown}")
# Deductions
if (
hasattr(evaluation, "deductions")
and evaluation.deductions
and evaluation.deductions.total > 0
):
print(f"\n⚠️ DEDUCTIONS: -{evaluation.deductions.total}")
print("-" * 30)
if evaluation.deductions.reasons:
print(f" {evaluation.deductions.reasons}")
# Key Strengths
if hasattr(evaluation, "key_strengths") and evaluation.key_strengths:
print(f"\n✅ KEY STRENGTHS:")
print("-" * 30)
for i, strength in enumerate(evaluation.key_strengths, 1):
print(f" {i}. {strength}")
# Areas for Improvement
if (
hasattr(evaluation, "areas_for_improvement")
and evaluation.areas_for_improvement
):
print(f"\n🔧 AREAS FOR IMPROVEMENT:")
print("-" * 30)
for i, area in enumerate(evaluation.areas_for_improvement, 1):
print(f" {i}. {area}")
print("\n" + "=" * 80)
def _evaluate_resume(
resume_data: JSONResume, github_data: dict = None, blog_data: dict = None
) -> Optional[EvaluationData]:
"""Evaluate the resume using AI and display results."""
model_params = MODEL_PARAMETERS.get(DEFAULT_MODEL)
evaluator = ResumeEvaluator(model_name=DEFAULT_MODEL, model_params=model_params)
# Convert JSON resume data to text
resume_text = convert_json_resume_to_text(resume_data)
# Add GitHub data if available
if github_data:
github_text = convert_github_data_to_text(github_data)
resume_text += github_text
# Add blog data if available
if blog_data:
blog_text = convert_blog_data_to_text(blog_data)
resume_text += blog_text
# Evaluate the enhanced resume
evaluation_result = evaluator.evaluate_resume(resume_text)
# print(evaluation_result)
return evaluation_result
def is_valid_resume_data(resume_data: JSONResume) -> bool:
"""Check if the resume data has at least some extracted core content."""
if not resume_data:
return False
core_sections = [
resume_data.basics,
resume_data.work,
resume_data.education,
resume_data.skills,
resume_data.projects,
]
return any(section is not None for section in core_sections)
def find_profile(profiles, network):
if not profiles:
return None
return next(
(p for p in profiles if p.network and p.network.lower() == network.lower()),
None,
)
def main(pdf_path):
# Create cache filename based on PDF path
cache_filename = (
f"cache/resumecache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
)
github_cache_filename = (
f"cache/githubcache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
)
resume_data = None
cache_loaded = False
# Check if cache exists and we're in development mode
if DEVELOPMENT_MODE and os.path.exists(cache_filename):
print(f"Loading cached data from {cache_filename}")
try:
cached_data = json.loads(Path(cache_filename).read_text(encoding="utf-8"))
loaded_resume = JSONResume(**cached_data)
if not is_valid_resume_data(loaded_resume):
raise ValueError("Cached resume data contains no core content")
resume_data = loaded_resume
cache_loaded = True
except Exception as e:
print(f"⚠️ Warning: Invalid cache file {cache_filename}: {e}")
print("Ignoring cache and reprocessing PDF...")
try:
os.remove(cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid cache file {cache_filename}: {delete_err}"
)
if not cache_loaded:
logger.debug(
f"Extracting data from PDF"
+ (" and caching to " + cache_filename if DEVELOPMENT_MODE else "")
)
pdf_handler = PDFHandler()
resume_data = pdf_handler.extract_json_from_pdf(pdf_path)
if resume_data == None:
return None
if DEVELOPMENT_MODE:
if is_valid_resume_data(resume_data):
os.makedirs(os.path.dirname(cache_filename), exist_ok=True)
Path(cache_filename).write_text(
json.dumps(resume_data.model_dump(), indent=2, ensure_ascii=False),
encoding="utf-8",
)
else:
logger.warning(
"Newly extracted resume data is empty/invalid. Skipping cache write."
)
# Check if cache exists and we're in development mode
github_data = {}
github_cache_loaded = False
if DEVELOPMENT_MODE and os.path.exists(github_cache_filename):
print(f"Loading cached data from {github_cache_filename}")
try:
loaded_github = json.loads(
Path(github_cache_filename).read_text(encoding="utf-8")
)
if (
not isinstance(loaded_github, dict)
or not loaded_github
or "profile" not in loaded_github
):
raise ValueError("Cached GitHub data is invalid or empty")
github_data = loaded_github
github_cache_loaded = True
except Exception as e:
print(f"⚠️ Warning: Invalid GitHub cache file {github_cache_filename}: {e}")
print("Ignoring GitHub cache and refetching...")
try:
os.remove(github_cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid GitHub cache file {github_cache_filename}: {delete_err}"
)
if not github_cache_loaded:
# Add validation to handle None values
profiles = []
if resume_data and hasattr(resume_data, "basics") and resume_data.basics:
profiles = resume_data.basics.profiles or []
github_profile = find_profile(profiles, "Github")
if github_profile:
print(
f"Fetching GitHub data"
+ (
" and caching to " + github_cache_filename
if DEVELOPMENT_MODE
else ""
)
)
github_data = fetch_and_display_github_info(github_profile.url)
if (
DEVELOPMENT_MODE
and github_data
and isinstance(github_data, dict)
and "profile" in github_data
):
os.makedirs(os.path.dirname(github_cache_filename), exist_ok=True)
Path(github_cache_filename).write_text(
json.dumps(github_data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
score = _evaluate_resume(resume_data, github_data)
# Get candidate name for display
candidate_name = os.path.basename(pdf_path).replace(".pdf", "")
if (
resume_data
and hasattr(resume_data, "basics")
and resume_data.basics
and resume_data.basics.name
):
candidate_name = resume_data.basics.name
# Print evaluation results in readable format
print_evaluation_results(score, candidate_name)
if DEVELOPMENT_MODE:
csv_row = transform_evaluation_response(
file_name=os.path.basename(pdf_path),
evaluation=score,
resume_data=resume_data,
github_data=github_data,
)
# Write CSV row to file
csv_path = "resume_evaluations.csv"
file_exists = os.path.exists(csv_path)
with open(csv_path, "a", newline="", encoding="utf-8") as csvfile:
fieldnames = list(csv_row.keys())
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write headers if file doesn't exist
if not file_exists:
writer.writeheader()
# Write the row
writer.writerow(csv_row)
return score
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python score.py <pdf_path>")
exit(1)
pdf_path = sys.argv[1]
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' does not exist.")
exit(1)
main(pdf_path)