|
""" |
|
Explanation Generator Module |
|
|
|
This module handles the generation of explanations for resume rankings |
|
using the QwQ-32B model from Hugging Face. |
|
""" |
|
|
|
import torch |
|
from transformers import AutoModelForCausalLM, AutoTokenizer |
|
import os |
|
import re |
|
|
|
class ExplanationGenerator: |
|
def __init__(self, model_name="Qwen/QwQ-32B"): |
|
"""Initialize the explanation generator with the specified model""" |
|
self.model_name = model_name |
|
self.model = None |
|
self.tokenizer = None |
|
self.initialized = False |
|
|
|
def load_model(self): |
|
"""Load the model and tokenizer if not already loaded""" |
|
if not self.initialized: |
|
try: |
|
|
|
if torch.cuda.is_available(): |
|
gpu_memory = torch.cuda.get_device_properties(0).total_memory |
|
|
|
if gpu_memory >= 32 * (1024**3): |
|
device = "cuda" |
|
else: |
|
device = "cpu" |
|
else: |
|
device = "cpu" |
|
|
|
|
|
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) |
|
|
|
|
|
if device == "cuda": |
|
self.model = AutoModelForCausalLM.from_pretrained( |
|
self.model_name, |
|
torch_dtype=torch.bfloat16, |
|
device_map="auto" |
|
) |
|
else: |
|
|
|
self.model = None |
|
print("Warning: Loading QwQ-32B on CPU is not recommended. Using template-based explanations instead.") |
|
|
|
self.initialized = True |
|
except Exception as e: |
|
print(f"Error loading QwQ-32B model: {str(e)}") |
|
print("Falling back to template-based explanations.") |
|
self.model = None |
|
self.initialized = True |
|
|
|
def generate_explanation(self, resume_text, job_description, score, semantic_score, keyword_score, skills): |
|
"""Generate explanation for why a resume was ranked highly""" |
|
|
|
if not self.initialized: |
|
self.load_model() |
|
|
|
|
|
if self.model is not None: |
|
try: |
|
|
|
prompt = self._create_prompt(resume_text, job_description, score, semantic_score, keyword_score, skills) |
|
|
|
|
|
messages = [ |
|
{"role": "user", "content": prompt} |
|
] |
|
|
|
|
|
text = self.tokenizer.apply_chat_template( |
|
messages, |
|
tokenize=False, |
|
add_generation_prompt=True |
|
) |
|
|
|
|
|
inputs = self.tokenizer(text, return_tensors="pt").to(self.model.device) |
|
|
|
|
|
output_ids = self.model.generate( |
|
**inputs, |
|
max_new_tokens=300, |
|
temperature=0.6, |
|
top_p=0.95, |
|
top_k=30 |
|
) |
|
|
|
|
|
response = self.tokenizer.decode(output_ids[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) |
|
|
|
|
|
cleaned_response = self._clean_response(response) |
|
|
|
return cleaned_response |
|
|
|
except Exception as e: |
|
print(f"Error generating explanation with QwQ-32B: {str(e)}") |
|
|
|
return self._generate_template_explanation(score, semantic_score, keyword_score, skills) |
|
else: |
|
|
|
return self._generate_template_explanation(score, semantic_score, keyword_score, skills) |
|
|
|
def _create_prompt(self, resume_text, job_description, score, semantic_score, keyword_score, skills): |
|
"""Create a prompt for the explanation generation""" |
|
|
|
resume_excerpt = resume_text[:1000] + "..." if len(resume_text) > 1000 else resume_text |
|
|
|
prompt = f"""You are an AI assistant helping a recruiter understand why a candidate's resume was matched with a job posting. |
|
|
|
The resume has been assigned the following scores: |
|
- Overall Match Score: {score:.2f} out of 1.0 |
|
- Semantic Relevance Score: {semantic_score:.2f} out of 1.0 |
|
- Keyword Match Score: {keyword_score:.2f} out of 1.0 |
|
|
|
The job description is: |
|
``` |
|
{job_description} |
|
``` |
|
|
|
Based on analysis, the resume contains these skills relevant to the job: {', '.join(skills)} |
|
|
|
Resume excerpt: |
|
``` |
|
{resume_excerpt} |
|
``` |
|
|
|
Please provide a short explanation (3-5 sentences) of why this resume received these scores and how well it matches the job requirements. Focus on the relationship between the candidate's experience and the job requirements.""" |
|
|
|
return prompt |
|
|
|
def _clean_response(self, response): |
|
"""Clean the response from the model""" |
|
|
|
response = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL) |
|
|
|
|
|
if len(response) > 500: |
|
sentences = response.split('.') |
|
shortened = '.'.join(sentences[:5]) + '.' |
|
return shortened |
|
|
|
return response |
|
|
|
def _generate_template_explanation(self, score, semantic_score, keyword_score, skills): |
|
"""Generate a template-based explanation when the model is not available""" |
|
|
|
if score > 0.8: |
|
quality = "excellent" |
|
elif score > 0.6: |
|
quality = "good" |
|
elif score > 0.4: |
|
quality = "moderate" |
|
else: |
|
quality = "limited" |
|
|
|
explanation = f"This resume shows {quality} alignment with the job requirements, with an overall score of {score:.2f}. " |
|
|
|
if semantic_score > keyword_score: |
|
explanation += f"The candidate's experience demonstrates strong semantic relevance ({semantic_score:.2f}) to the position, though specific keyword matches ({keyword_score:.2f}) could be improved. " |
|
else: |
|
explanation += f"The resume contains many relevant keywords ({keyword_score:.2f}), but could benefit from better contextual alignment ({semantic_score:.2f}) with the job requirements. " |
|
|
|
if skills: |
|
if len(skills) > 3: |
|
explanation += f"Key skills identified include {', '.join(skills[:3])}, and {len(skills)-3} others that match the job requirements." |
|
else: |
|
explanation += f"Key skills identified include {', '.join(skills)}." |
|
else: |
|
explanation += "No specific skills were identified that directly match the requirements." |
|
|
|
return explanation |