mirror of
https://github.com/vee1e/hiring-agent.git
synced 2026-09-02 03:07:17 +00:00
When processing multiple PDFs, memory usage was growing continuously because pymupdf.open() creates document objects that hold file handles and buffers. These weren't being released properly. Changed to use 'with' statement which automatically closes the document after use, even if an exception occurs.
321 lines
11 KiB
Python
321 lines
11 KiB
Python
import os
|
|
import sys
|
|
import json
|
|
import time
|
|
import logging
|
|
import pymupdf
|
|
|
|
from models import (
|
|
JSONResume,
|
|
Basics,
|
|
Work,
|
|
Education,
|
|
Skill,
|
|
Project,
|
|
Award,
|
|
BasicsSection,
|
|
WorkSection,
|
|
EducationSection,
|
|
SkillsSection,
|
|
ProjectsSection,
|
|
AwardsSection,
|
|
)
|
|
from llm_utils import initialize_llm_provider, extract_json_from_response
|
|
from pymupdf_rag import to_markdown
|
|
from typing import List, Optional, Dict, Any
|
|
from prompt import (
|
|
DEFAULT_MODEL,
|
|
MODEL_PARAMETERS,
|
|
MODEL_PROVIDER_MAPPING,
|
|
GEMINI_API_KEY,
|
|
)
|
|
from prompts.template_manager import TemplateManager
|
|
from transform import transform_parsed_data
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PDFHandler:
|
|
|
|
def __init__(self):
|
|
self.template_manager = TemplateManager()
|
|
self._initialize_llm_provider()
|
|
|
|
def _initialize_llm_provider(self):
|
|
"""Initialize the appropriate LLM provider based on the model."""
|
|
self.provider = initialize_llm_provider(DEFAULT_MODEL)
|
|
|
|
def extract_text_from_pdf(self, pdf_path: str) -> Optional[str]:
|
|
try:
|
|
if not os.path.exists(pdf_path):
|
|
raise FileNotFoundError(f"PDF file not found: {pdf_path}")
|
|
|
|
with pymupdf.open(pdf_path) as doc:
|
|
pages = range(doc.page_count)
|
|
resume_text = to_markdown(
|
|
doc,
|
|
pages=pages,
|
|
)
|
|
logger.debug(
|
|
f"Extracted text from PDF: {len(resume_text) if resume_text else 0} characters"
|
|
)
|
|
return resume_text
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while reading the PDF: {e}")
|
|
return None
|
|
|
|
def _call_llm_for_section(
|
|
self, section_name: str, text_content: str, prompt: str, return_model=None
|
|
) -> Optional[Dict]:
|
|
try:
|
|
start_time = time.time()
|
|
logger.debug(
|
|
f"🔄 Extracting {section_name} section using {DEFAULT_MODEL}..."
|
|
)
|
|
|
|
model_params = MODEL_PARAMETERS.get(
|
|
DEFAULT_MODEL, {"temperature": 0.1, "top_p": 0.9}
|
|
)
|
|
|
|
section_system_message = self.template_manager.render_template(
|
|
"system_message", section_name_param=section_name
|
|
)
|
|
if not section_system_message:
|
|
logger.error(
|
|
f"❌ Failed to render system message template for {section_name}"
|
|
)
|
|
return None
|
|
|
|
chat_params = {
|
|
"model": DEFAULT_MODEL,
|
|
"messages": [
|
|
{"role": "system", "content": section_system_message},
|
|
{"role": "user", "content": prompt},
|
|
],
|
|
"options": {
|
|
"stream": False,
|
|
"temperature": model_params["temperature"],
|
|
"top_p": model_params["top_p"],
|
|
},
|
|
}
|
|
|
|
kwargs = {}
|
|
if return_model:
|
|
kwargs["format"] = return_model.model_json_schema()
|
|
|
|
# Use the appropriate provider to make the API call
|
|
response = self.provider.chat(**chat_params, **kwargs)
|
|
|
|
response_text = response["message"]["content"]
|
|
|
|
try:
|
|
response_text = extract_json_from_response(response_text)
|
|
json_start = response_text.find("{")
|
|
json_end = response_text.rfind("}")
|
|
if json_start != -1 and json_end != -1:
|
|
response_text = response_text[json_start : json_end + 1]
|
|
parsed_data = json.loads(response_text)
|
|
logger.debug(f"✅ Successfully extracted {section_name} section")
|
|
|
|
transformed_data = transform_parsed_data(parsed_data)
|
|
end_time = time.time()
|
|
total_time = end_time - start_time
|
|
logger.debug(
|
|
f"⏱️ Total time for separate section extraction: {total_time:.2f} seconds"
|
|
)
|
|
|
|
return transformed_data
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"❌ Error parsing JSON for {section_name} section: {e}")
|
|
logger.error(f"Raw response: {response_text}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error calling LLM for {section_name} section: {e}")
|
|
return None
|
|
|
|
def extract_basics_section(self, resume_text: str) -> Optional[Dict]:
|
|
prompt = self.template_manager.render_template(
|
|
"basics", text_content=resume_text
|
|
)
|
|
if not prompt:
|
|
logger.error("❌ Failed to render basics template")
|
|
return None
|
|
return self._call_llm_for_section("basics", resume_text, prompt, BasicsSection)
|
|
|
|
def extract_work_section(self, resume_text: str) -> Optional[Dict]:
|
|
prompt = self.template_manager.render_template("work", text_content=resume_text)
|
|
if not prompt:
|
|
logger.error("❌ Failed to render work template")
|
|
return None
|
|
return self._call_llm_for_section("work", resume_text, prompt, WorkSection)
|
|
|
|
def extract_education_section(self, resume_text: str) -> Optional[Dict]:
|
|
prompt = self.template_manager.render_template(
|
|
"education", text_content=resume_text
|
|
)
|
|
if not prompt:
|
|
logger.error("❌ Failed to render education template")
|
|
return None
|
|
return self._call_llm_for_section(
|
|
"education", resume_text, prompt, EducationSection
|
|
)
|
|
|
|
def extract_skills_section(self, resume_text: str) -> Optional[Dict]:
|
|
prompt = self.template_manager.render_template(
|
|
"skills", text_content=resume_text
|
|
)
|
|
if not prompt:
|
|
logger.error("❌ Failed to render skills template")
|
|
return None
|
|
return self._call_llm_for_section("skills", resume_text, prompt, SkillsSection)
|
|
|
|
def extract_projects_section(self, resume_text: str) -> Optional[Dict]:
|
|
prompt = self.template_manager.render_template(
|
|
"projects", text_content=resume_text
|
|
)
|
|
if not prompt:
|
|
logger.error("❌ Failed to render projects template")
|
|
return None
|
|
return self._call_llm_for_section(
|
|
"projects", resume_text, prompt, ProjectsSection
|
|
)
|
|
|
|
def extract_awards_section(self, resume_text: str) -> Optional[Dict]:
|
|
prompt = self.template_manager.render_template(
|
|
"awards", text_content=resume_text
|
|
)
|
|
if not prompt:
|
|
logger.error("❌ Failed to render awards template")
|
|
return None
|
|
return self._call_llm_for_section("awards", resume_text, prompt, AwardsSection)
|
|
|
|
def extract_json_from_text(self, resume_text: str) -> Optional[JSONResume]:
|
|
try:
|
|
return self._extract_all_sections_separately(resume_text)
|
|
except Exception as e:
|
|
logger.error(f"Error calling Ollama: {e}")
|
|
return None
|
|
|
|
def extract_json_from_pdf(self, pdf_path: str) -> Optional[JSONResume]:
|
|
try:
|
|
logger.debug(f"📄 Extracting text from PDF: {pdf_path}")
|
|
text_content = self.extract_text_from_pdf(pdf_path)
|
|
|
|
if not text_content:
|
|
logger.error("❌ Failed to extract text from PDF")
|
|
return None
|
|
|
|
logger.debug(
|
|
f"✅ Successfully extracted {len(text_content)} characters from PDF"
|
|
)
|
|
|
|
logger.debug("🔄 Extracting all sections separately...")
|
|
return self._extract_all_sections_separately(text_content)
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error during PDF to JSON extraction: {e}")
|
|
return None
|
|
|
|
def _extract_section_data(
|
|
self, text_content: str, section_name: str, return_model=None
|
|
) -> Optional[Dict]:
|
|
section_extractors = {
|
|
"basics": self.extract_basics_section,
|
|
"work": self.extract_work_section,
|
|
"education": self.extract_education_section,
|
|
"skills": self.extract_skills_section,
|
|
"projects": self.extract_projects_section,
|
|
"awards": self.extract_awards_section,
|
|
}
|
|
|
|
if section_name not in section_extractors:
|
|
logger.error(f"❌ Invalid section name: {section_name}")
|
|
logger.error(f"Valid sections: {list(section_extractors.keys())}")
|
|
return None
|
|
|
|
return section_extractors[section_name](text_content)
|
|
|
|
def _extract_single_section(
|
|
self, text_content: str, section_name: str, return_model=None
|
|
) -> Optional[Dict]:
|
|
section_data = self._extract_section_data(
|
|
text_content, section_name, return_model
|
|
)
|
|
if section_data:
|
|
complete_resume = {
|
|
"basics": None,
|
|
"work": None,
|
|
"volunteer": None,
|
|
"education": None,
|
|
"awards": None,
|
|
"certificates": None,
|
|
"publications": None,
|
|
"skills": None,
|
|
"languages": None,
|
|
"interests": None,
|
|
"references": None,
|
|
"projects": None,
|
|
"meta": None,
|
|
}
|
|
|
|
complete_resume.update(section_data)
|
|
return complete_resume
|
|
|
|
return None
|
|
|
|
def _extract_all_sections_separately(
|
|
self, text_content: str
|
|
) -> Optional[JSONResume]:
|
|
start_time = time.time()
|
|
|
|
sections = ["basics", "work", "education", "skills", "projects", "awards"]
|
|
|
|
complete_resume = {
|
|
"basics": None,
|
|
"work": None,
|
|
"volunteer": None,
|
|
"education": None,
|
|
"awards": None,
|
|
"certificates": None,
|
|
"publications": None,
|
|
"skills": None,
|
|
"languages": None,
|
|
"interests": None,
|
|
"references": None,
|
|
"projects": None,
|
|
"meta": None,
|
|
}
|
|
|
|
for section_name in sections:
|
|
section_data = self._extract_section_data(text_content, section_name)
|
|
|
|
if section_data:
|
|
complete_resume.update(section_data)
|
|
logger.debug(f"✅ Successfully extracted {section_name} section")
|
|
else:
|
|
logger.error(f"⚠️ Failed to extract {section_name} section")
|
|
|
|
try:
|
|
if complete_resume.get("basics") and isinstance(
|
|
complete_resume["basics"], dict
|
|
):
|
|
try:
|
|
complete_resume["basics"] = Basics(**complete_resume["basics"])
|
|
except Exception as e:
|
|
logger.error(f"❌ Error creating Basics object: {e}")
|
|
complete_resume["basics"] = None
|
|
|
|
json_resume = JSONResume(**complete_resume)
|
|
|
|
end_time = time.time()
|
|
total_time = end_time - start_time
|
|
logger.info(
|
|
f"⏱️ Total time for separate section extraction: {total_time:.2f} seconds"
|
|
)
|
|
|
|
return json_resume
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error creating JSONResume object: {e}")
|
|
return None
|