|
|
import os |
|
|
import json |
|
|
import re |
|
|
import logging |
|
|
import fitz |
|
|
import google.generativeai as genai |
|
|
from dotenv import load_dotenv |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
api_key = os.getenv("GEMINI_API_KEY") |
|
|
if not api_key: |
|
|
raise ValueError("GEMINI_API_KEY is missing.") |
|
|
|
|
|
genai.configure(api_key=api_key) |
|
|
|
|
|
def extract_text_from_stream(file_bytes: bytes) -> str: |
|
|
text = "" |
|
|
try: |
|
|
with fitz.open(stream=file_bytes, filetype="pdf") as doc: |
|
|
for page in doc: |
|
|
text += page.get_text() |
|
|
except Exception as e: |
|
|
logger.error(f"PDF Extraction Error: {e}") |
|
|
raise ValueError("Failed to extract text from PDF.") |
|
|
return text |
|
|
|
|
|
def get_available_model_name(): |
|
|
""" |
|
|
Dynamically finds a working model from the user's account. |
|
|
""" |
|
|
try: |
|
|
available_models = [] |
|
|
for m in genai.list_models(): |
|
|
if 'generateContent' in m.supported_generation_methods: |
|
|
available_models.append(m.name) |
|
|
|
|
|
if not available_models: |
|
|
logger.error("No models found.") |
|
|
return None |
|
|
|
|
|
|
|
|
preferred_order = [ |
|
|
"models/gemini-1.5-flash", |
|
|
"models/gemini-1.5-pro", |
|
|
"models/gemini-pro", |
|
|
"models/gemini-1.0-pro" |
|
|
] |
|
|
|
|
|
|
|
|
for preferred in preferred_order: |
|
|
if preferred in available_models: |
|
|
logger.info(f"Selected Preferred Model: {preferred}") |
|
|
return preferred |
|
|
|
|
|
|
|
|
fallback = available_models[0] |
|
|
logger.warning(f"Preferred models missing. Falling back to: {fallback}") |
|
|
return fallback |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error listing models: {e}") |
|
|
return None |
|
|
|
|
|
def analyze_resume(resume_text: str, job_description: str = None) -> dict: |
|
|
|
|
|
|
|
|
model_name = get_available_model_name() |
|
|
if not model_name: |
|
|
return {"error": "CRITICAL: No available AI models found for this API Key."} |
|
|
|
|
|
|
|
|
if job_description: |
|
|
prompt = f""" |
|
|
Act as a strict AI Recruiter. Compare the Resume against the Job Description. |
|
|
|
|
|
RETURN JSON ONLY with this exact structure: |
|
|
{{ |
|
|
"candidate": {{ |
|
|
"name": "string", |
|
|
"email": "string", |
|
|
"phone": "string", |
|
|
"skills": ["list", "of", "candidate", "skills"], |
|
|
"experience_years": "string or null" |
|
|
}}, |
|
|
"match_analysis": {{ |
|
|
"score": integer_0_to_100, |
|
|
"reasoning": "brief summary of why this score was given", |
|
|
"matching_skills": ["skills in both resume and JD"], |
|
|
"missing_skills": ["skills in JD but NOT in resume"], |
|
|
"verdict": "Interview" | "Shortlist" | "Reject" |
|
|
}} |
|
|
}} |
|
|
|
|
|
JOB DESCRIPTION: |
|
|
{job_description[:5000]} |
|
|
|
|
|
RESUME TEXT: |
|
|
{resume_text[:10000]} |
|
|
""" |
|
|
else: |
|
|
prompt = f""" |
|
|
Extract structured data from the resume. Return JSON: |
|
|
{{ |
|
|
"candidate": {{ |
|
|
"name": "string", |
|
|
"email": "string", |
|
|
"phone": "string", |
|
|
"skills": ["list", "of", "skills"], |
|
|
"summary": "string" |
|
|
}} |
|
|
}} |
|
|
|
|
|
RESUME TEXT: |
|
|
{resume_text[:10000]} |
|
|
""" |
|
|
|
|
|
|
|
|
try: |
|
|
model = genai.GenerativeModel(model_name) |
|
|
response = model.generate_content(prompt) |
|
|
|
|
|
raw = response.text.strip() |
|
|
clean_json = re.sub(r'```json\s*|```', '', raw, flags=re.MULTILINE).strip() |
|
|
return json.loads(clean_json) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Analysis failed with model {model_name}: {e}") |
|
|
return {"error": f"Analysis failed using {model_name}. Detail: {str(e)}"} |