ResumeExtractor2 / utils /mistral.py
WebashalarForML's picture
Update utils/mistral.py
7b20463 verified
# mistral.py
import os
import json
import logging
import requests
from dotenv import load_dotenv
from utils.fileTotext import extract_text_based_on_format
import re
from utils.spacy import Parser_from_model
# Load environment variables from .env file
load_dotenv()
# Authenticate with Groq
GROQ_API_KEY = os.getenv('GROQ_API_KEY')
if not GROQ_API_KEY:
raise ValueError("Groq API key is not set in environment variables.")
GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
MODEL_NAME = "llama-3.3-70b-versatile" # you can switch to mixtral if needed
# 🔥 Groq LLM Call (Replacement for HuggingFace)
def call_llm(messages, max_tokens=2048, temperature=0.3):
headers = {
"Authorization": f"Bearer {GROQ_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": MODEL_NAME,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
response = requests.post(GROQ_API_URL, headers=headers, json=payload)
if response.status_code != 200:
raise Exception(f"Groq API Error: {response.status_code} | {response.text}")
result = response.json()
return result["choices"][0]["message"]["content"]
# Function to clean model output
# def Data_Cleaner(text):
# pattern = r".*?format:"
# result = re.split(pattern, text, maxsplit=1)
# if len(result) > 1:
# text_after_format = result[1].strip().strip('`').strip('json')
# else:
# text_after_format = text.strip().strip('`').strip('json')
# try:
# json.loads(text_after_format)
# return text_after_format
# except json.JSONDecodeError:
# logging.error("Data cleaning led to invalid JSON")
# return text
def Data_Cleaner(text):
try:
# Extract JSON block using regex
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if json_match:
json_str = json_match.group(0)
return json_str
else:
raise ValueError("No JSON found in response")
except Exception as e:
logging.error(f"JSON extraction failed: {e}")
return None
# Function to call LLM and process output
def Model_ProfessionalDetails_Output(resume, client=None):
system_role = {
"role": "system",
"content": "You are a skilled resume parser. Your task is to extract professional details from resumes in a structured JSON format defined by the User. Ensure accuracy and completeness while maintaining the format provided and if field are missing just return 'not found'."
}
user_prompt = {
"role": "user",
"content": f'''Act as a resume parser for the following text given in text: {resume}
Extract the text in the following output JSON string as:
{{
"professional": {{
"technical_skills": "...",
"non_technical_skills": "...",
"tools": "...",
"projects": "...",
"projects_experience": "...",
"experience": "...",
"companies_worked_at": "...",
"certifications": "...",
"roles": "...",
"qualifications": "...",
"courses": "...",
"university": "...",
"year_of_graduation": "..."
}}
}}
Json Output:
'''
}
try:
response = call_llm([system_role, user_prompt], max_tokens=3000, temperature=0.35)
clean_response = Data_Cleaner(response)
if not clean_response:
raise ValueError("Empty or invalid LLM response")
parsed_response = json.loads(clean_response)
except Exception as e:
logging.error(f"LLM Error: {e}")
return {}
return parsed_response
def Model_PersonalDetails_Output(resume, client=None):
system_role = {
"role": "system",
"content": "You are a skilled resume parser. Your task is to extract professional details from resumes in a structured JSON format defined by the User. Ensure accuracy and completeness while maintaining the format provided and if field are missing just return 'not found'."
}
user_prompt = {
"role": "user",
"content": f'''Act as a resume parser for the following text given in text: {resume}
Extract the text in the following output JSON string as:
{{
"personal": {{
"name": "...",
"contact_number": "...",
"email": "...",
"Address": "...",
"link": "..."
}}
}}
output:
'''
}
try:
response = call_llm([system_role, user_prompt], max_tokens=2000, temperature=0.35)
clean_response = Data_Cleaner(response)
if not clean_response:
raise ValueError("Empty or invalid LLM response")
parsed_response = json.loads(clean_response)
except Exception as e:
print("JSON Decode Error:", e)
return {}
return parsed_response
# ------------------- REST OF YOUR CODE UNCHANGED -------------------
linkedin_pattern = r"https?://(?:www\.)?linkedin\.com/[\w\-_/]+"
github_pattern = r"https?://(?:www\.)?github\.com/[\w\-_/]+"
email_pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
contact_pattern = r"^\+?[\d\s\-()]{7,15}$"
def extract_links(hyperlinks):
linkedin_links = []
github_links = []
for link in hyperlinks:
if re.match(linkedin_pattern, link):
linkedin_links.append(link)
elif re.match(github_pattern, link):
github_links.append(link)
return linkedin_links, github_links
def is_valid_email(email):
email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
return re.match(email_regex, email) is not None
def is_valid_contact(contact):
patterns = [
r'^\+91[\s\.\-\/]?\(?0?\)?[\s\-\.\/]?\d{5}[\s\-\.\/]?\d{5}$', # +91 with optional 0 and separators
r'^\+91[\s\.\-\/]?\d{5}[\s\-\.\/]?\d{5}$', # +91 with 10 digits separated
r'^\d{5}[\s\-\.\/]?\d{5}$', # Local format without country code
r'^\+91[\s\.\-\/]?\d{10}$', # +91 with 10 digits together
r'^\d{10}$', # 10 digits together
r'^\+91[\s\.\-\/]?\(?\d{5}\)?[\s\-\.\/]?\d{5}[\s\-\.\/]?\d{5}$' # +91 with varying separators
r'\+1\s\(\d{3}\)\s\d{3}-\d{4} ', # USA/Canada Intl +1 (XXX) XXX-XXXX
r'\(\d{3}\)\s\d{3}-\d{4} ', # USA/Canada STD (XXX) XXX-XXXX
r'\(\d{3}\)\s\d{3}\s\d{4} ', # USA/Canada (XXX) XXX XXXX
r'\(\d{3}\)\s\d{3}\s\d{3} ', # USA/Canada (XXX) XXX XXX
r'\+1\d{10} ', # +1 XXXXXXXXXX
r'\d{10} ', # XXXXXXXXXX
r'\+44\s\d{4}\s\d{6} ', # UK Intl +44 XXXX XXXXXX
r'\+44\s\d{3}\s\d{3}\s\d{4} ', # UK Intl +44 XXX XXX XXXX
r'0\d{4}\s\d{6} ', # UK STD 0XXXX XXXXXX
r'0\d{3}\s\d{3}\s\d{4} ', # UK STD 0XXX XXX XXXX
r'\+44\d{10} ', # +44 XXXXXXXXXX
r'0\d{10} ', # 0XXXXXXXXXX
r'\+61\s\d\s\d{4}\s\d{4} ', # Australia Intl +61 X XXXX XXXX
r'0\d\s\d{4}\s\d{4} ', # Australia STD 0X XXXX XXXX
r'\+61\d{9} ', # +61 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+91\s\d{5}-\d{5} ', # India Intl +91 XXXXX-XXXXX
r'\+91\s\d{4}-\d{6} ', # India Intl +91 XXXX-XXXXXX
r'\+91\s\d{10} ', # India Intl +91 XXXXXXXXXX
r'0\d{2}-\d{7} ', # India STD 0XX-XXXXXXX
r'\+91\d{10} ', # +91 XXXXXXXXXX
r'\+49\s\d{4}\s\d{8} ', # Germany Intl +49 XXXX XXXXXXXX
r'\+49\s\d{3}\s\d{7} ', # Germany Intl +49 XXX XXXXXXX
r'0\d{3}\s\d{8} ', # Germany STD 0XXX XXXXXXXX
r'\+49\d{12} ', # +49 XXXXXXXXXXXX
r'\+49\d{10} ', # +49 XXXXXXXXXX
r'0\d{11} ', # 0XXXXXXXXXXX
r'\+86\s\d{3}\s\d{4}\s\d{4} ', # China Intl +86 XXX XXXX XXXX
r'0\d{3}\s\d{4}\s\d{4} ', # China STD 0XXX XXXX XXXX
r'\+86\d{11} ', # +86 XXXXXXXXXXX
r'\+81\s\d\s\d{4}\s\d{4} ', # Japan Intl +81 X XXXX XXXX
r'\+81\s\d{2}\s\d{4}\s\d{4} ', # Japan Intl +81 XX XXXX XXXX
r'0\d\s\d{4}\s\d{4} ', # Japan STD 0X XXXX XXXX
r'\+81\d{10} ', # +81 XXXXXXXXXX
r'\+81\d{9} ', # +81 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+55\s\d{2}\s\d{5}-\d{4} ', # Brazil Intl +55 XX XXXXX-XXXX
r'\+55\s\d{2}\s\d{4}-\d{4} ', # Brazil Intl +55 XX XXXX-XXXX
r'0\d{2}\s\d{4}\s\d{4} ', # Brazil STD 0XX XXXX XXXX
r'\+55\d{11} ', # +55 XXXXXXXXXXX
r'\+55\d{10} ', # +55 XXXXXXXXXX
r'0\d{10} ', # 0XXXXXXXXXX
r'\+33\s\d\s\d{2}\s\d{2}\s\d{2}\s\d{2} ', # France Intl +33 X XX XX XX XX
r'0\d\s\d{2}\s\d{2}\s\d{2}\s\d{2} ', # France STD 0X XX XX XX XX
r'\+33\d{9} ', # +33 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+7\s\d{3}\s\d{3}-\d{2}-\d{2} ', # Russia Intl +7 XXX XXX-XX-XX
r'8\s\d{3}\s\d{3}-\d{2}-\d{2} ', # Russia STD 8 XXX XXX-XX-XX
r'\+7\d{10} ', # +7 XXXXXXXXXX
r'8\d{10} ', # 8 XXXXXXXXXX
r'\+27\s\d{2}\s\d{3}\s\d{4} ', # South Africa Intl +27 XX XXX XXXX
r'0\d{2}\s\d{3}\s\d{4} ', # South Africa STD 0XX XXX XXXX
r'\+27\d{9} ', # +27 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+52\s\d{3}\s\d{3}\s\d{4} ', # Mexico Intl +52 XXX XXX XXXX
r'\+52\s\d{2}\s\d{4}\s\d{4} ', # Mexico Intl +52 XX XXXX XXXX
r'01\s\d{3}\s\d{4} ', # Mexico STD 01 XXX XXXX
r'\+52\d{10} ', # +52 XXXXXXXXXX
r'01\d{7} ', # 01 XXXXXXX
r'\+234\s\d{3}\s\d{3}\s\d{4} ', # Nigeria Intl +234 XXX XXX XXXX
r'0\d{3}\s\d{3}\s\d{4} ', # Nigeria STD 0XXX XXX XXXX
r'\+234\d{10} ', # +234 XXXXXXXXXX
r'0\d{10} ', # 0XXXXXXXXXX
r'\+971\s\d\s\d{3}\s\d{4} ', # UAE Intl +971 X XXX XXXX
r'0\d\s\d{3}\s\d{4} ', # UAE STD 0X XXX XXXX
r'\+971\d{8} ', # +971 XXXXXXXX
r'0\d{8} ', # 0XXXXXXXX
r'\+54\s9\s\d{3}\s\d{3}\s\d{4} ', # Argentina Intl +54 9 XXX XXX XXXX
r'\+54\s\d{1}\s\d{4}\s\d{4} ', # Argentina Intl +54 X XXXX XXXX
r'0\d{3}\s\d{4} ', # Argentina STD 0XXX XXXX
r'\+54\d{10} ', # +54 9 XXXXXXXXXX
r'\+54\d{9} ', # +54 XXXXXXXXX
r'0\d{7} ', # 0XXXXXXX
r'\+966\s\d\s\d{3}\s\d{4} ', # Saudi Intl +966 X XXX XXXX
r'0\d\s\d{3}\s\d{4} ', # Saudi STD 0X XXX XXXX
r'\+966\d{8} ', # +966 XXXXXXXX
r'0\d{8} ', # 0XXXXXXXX
r'\+1\d{10} ', # +1 XXXXXXXXXX
r'\+1\s\d{3}\s\d{3}\s\d{4} ', # +1 XXX XXX XXXX
r'\d{5}\s\d{5} ', # XXXXX XXXXX
r'\d{10} ', # XXXXXXXXXX
r'\+44\d{10} ', # +44 XXXXXXXXXX
r'0\d{10} ', # 0XXXXXXXXXX
r'\+61\d{9} ', # +61 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+91\d{10} ', # +91 XXXXXXXXXX
r'\+49\d{12} ', # +49 XXXXXXXXXXXX
r'\+49\d{10} ', # +49 XXXXXXXXXX
r'0\d{11} ', # 0XXXXXXXXXXX
r'\+86\d{11} ', # +86 XXXXXXXXXXX
r'\+81\d{10} ', # +81 XXXXXXXXXX
r'\+81\d{9} ', # +81 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+55\d{11} ', # +55 XXXXXXXXXXX
r'\+55\d{10} ', # +55 XXXXXXXXXX
r'0\d{10} ', # 0XXXXXXXXXX
r'\+33\d{9} ', # +33 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX
r'\+7\d{10} ', # +7 XXXXXXXXXX
r'8\d{10} ', # 8 XXXXXXXXXX
r'\+27\d{9} ', # +27 XXXXXXXXX
r'0\d{9} ', # 0XXXXXXXXX (South Africa STD)
r'\+52\d{10} ', # +52 XXXXXXXXXX
r'01\d{7} ', # 01 XXXXXXX
r'\+234\d{10} ', # +234 XXXXXXXXXX
r'0\d{10} ', # 0XXXXXXXXXX
r'\+971\d{8} ', # +971 XXXXXXXX
r'0\d{8} ', # 0XXXXXXXX
r'\+54\s9\s\d{10} ', # +54 9 XXXXXXXXXX
r'\+54\d{9} ', # +54 XXXXXXXXX
r'0\d{7} ', # 0XXXXXXX
r'\+966\d{8} ', # +966 XXXXXXXX
r'0\d{8}' # 0XXXXXXXX
]
# Check if the contact matches any of the patterns
return any(re.match(pattern, contact) for pattern in patterns) is not None
def validate_contact_email(personal_data):
contact = personal_data.get('contact', 'Not found')
email = personal_data.get('email', 'Not found')
valid_contact = is_valid_contact(contact) if contact != 'Not found' else False
valid_email = is_valid_email(email) if email != 'Not found' else False
invalid_contact = 'Invalid contact' if not valid_contact else 'Valid contact'
invalid_email = 'Invalid email' if not valid_email else 'Valid email'
return valid_contact, invalid_contact, valid_email, invalid_email
def process_resume_data(file_path):
resume_text, hyperlinks = extract_text_based_on_format(file_path)
print("Resume converted to text successfully.")
if not resume_text:
return {"error": "Text extraction failed"}
linkedin_links, github_links = extract_links(hyperlinks)
try:
per_data = Model_PersonalDetails_Output(resume_text)
pro_data = Model_ProfessionalDetails_Output(resume_text)
if not per_data:
per_data = {}
if not pro_data:
pro_data = {}
result = {
"personal": {
"name": per_data.get('personal', {}).get('name', 'Not found'),
"contact": per_data.get('personal', {}).get('contact_number', 'Not found'),
"email": per_data.get('personal', {}).get('email', 'Not found'),
"location": per_data.get('personal', {}).get('Address', 'Not found'),
"linkedin": linkedin_links,
"github": github_links,
"other_links": hyperlinks
},
"professional": {
"technical_skills": pro_data.get('professional', {}).get('technical_skills', 'Not found'),
"non_technical_skills": pro_data.get('professional', {}).get('non_technical_skills', 'Not found'),
"tools": pro_data.get('professional', {}).get('tools', 'Not found'),
"experience": [
{
"company": pro_data.get('professional', {}).get('companies_worked_at', 'Not found'),
"projects": pro_data.get('professional', {}).get('projects', 'Not found'),
"role": pro_data.get('professional', {}).get('worked_as', 'Not found'),
"years": pro_data.get('professional', {}).get('experience', 'Not found'),
"project_experience": pro_data.get('professional', {}).get('projects_experience', 'Not found')
}
],
"education": [
{
"qualification": pro_data.get('professional', {}).get('qualification', 'Not found'),
"university": pro_data.get('professional', {}).get('university', 'Not found'),
"course": pro_data.get('professional', {}).get('course', 'Not found'),
"certificate": pro_data.get('professional', {}).get('certification', 'Not found')
}
]
}
}
valid_contact, invalid_contact, valid_email, invalid_email = validate_contact_email(result['personal'])
result['personal']['valid_contact'] = valid_contact
result['personal']['invalid_contact'] = invalid_contact
result['personal']['valid_email'] = valid_email
result['personal']['invalid_email'] = invalid_email
if per_data or pro_data:
print("---------LLM (Groq)-------")
return result
else:
raise ValueError("LLM returned no output")
except Exception as e:
logging.error(f"LLM failed: {e}. Falling back to SpaCy.")
print("---------SpaCy-------")
return Parser_from_model(file_path)