Files
2026-07-13 12:29:44 +08:00

324 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. Aborting extraction to prevent partial/invalid resume data."
)
return None
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