# mistral.py import os import json import logging from huggingface_hub import InferenceClient from huggingface_hub.utils._errors import BadRequestError 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 Hugging Face HFT = os.getenv('HF_TOKEN') if not HFT: raise ValueError("Hugging Face token is not set in environment variables.") client = InferenceClient(model="mistralai/Mistral-Nemo-Instruct-2407", token=HFT) # 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') return text_after_format # Function to call Mistral and process output def Model_ProfessionalDetails_Output(resume, client): 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": {{ "skills": "Extract and list all technical skills, non-technical skills, programming languages, frameworks, domains, and technologies based on the resume.", "soft_skills": "Extract non-technical skills, Communication skills, and soft skills based on the resume." "projects": "Include only the project names, titles, or headers mentioned in the resume. ", "projects_experience": ["Include overall project Experiences and about project in short mentioned in the resume.] ", "experience": "Include the total experience in months or years as mentioned in the resume.", "companies_worked_at": "Include the names of all companies worked at according to the resume. ", "certification": "Include any certifications obtained based on the resume. ", "worked_as": "Include the names of roles worked as according to the resume.", "qualification":"Extract and list the qualifications based on the resume, (qualifications likes B.Tech). If none are found, return 'No education listed'.", "course": "Extract the name of the Learning Course completed based on the resume. If not found, return 'No Course listed'.", "university": "Extract the name of the university or Collage or Intitute attended based on the resume. If not found, return 'No university listed'.", "year_of_graduation": "Extract the year of graduation from the resume. If not found, return 'No year of graduation listed'." }} }} Json Output: ''' } response = "" for message in client.chat_completion(messages=[system_role, user_prompt], max_tokens=3000, stream=True, temperature=0.35): response += message.choices[0].delta.content try: clean_response = Data_Cleaner(response) parsed_response = json.loads(clean_response) except json.JSONDecodeError as e: logging.error(f"JSON Decode Error: {e}") return {} return parsed_response def Model_PersonalDetails_Output(resume, client): 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": "Extract the full name based on the resume. If not found, return 'No name listed'.", "contact_number": "Extract the contact number from the resume. If not found, return 'No contact number listed'.", "email": "Extract the email address from the resume. If not found, return 'No email listed'.", "Address": "Extract the Address or address from the resume. If not found, return 'No Address listed'.", "link": "Extract any relevant links (e.g., portfolio, LinkedIn) from the resume. If not found, return 'No link listed'." }} }} output: ''' } # Response response = "" for message in client.chat_completion( messages=[system_role, user_prompt], max_tokens=3000, stream=True, temperature=0.35, ): response += message.choices[0].delta.content # Handle cases where the response might have formatting issues try: #print('The Og response:-->',response) clean_response=Data_Cleaner(response) #print("After data cleaning",clean_response) parsed_response = json.loads(clean_response) except json.JSONDecodeError as e: print("JSON Decode Error:", e) print("Raw Response:", response) return {} return parsed_response # # Fallback to SpaCy if Mistral fails 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"} # Attempt to use Mistral model for parsing try: # Extract personal details using Mistral per_data = Model_PersonalDetails_Output(resume_text, client) # Extract professional details using Mistral pro_data = Model_ProfessionalDetails_Output(resume_text, client) # Check if per_data and pro_data have been populated correctly if not per_data: logging.warning("Mistral personal data extraction failed.") per_data = {} if not pro_data: logging.warning("Mistral professional data extraction failed.") pro_data = {} # Combine both personal and professional details into a structured output 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'), "link": hyperlinks }, "professional": { "skills": pro_data.get('professional', {}).get('skills', 'Not found'), "soft_skills": pro_data.get('professional', {}).get('soft_skills', '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') } ] } } # If Mistral produces valid output, return it if per_data or pro_data: print("------Mistral-----") return result else: raise ValueError("Mistral returned no output") # Handle HuggingFace API or Mistral model errors except BadRequestError as e: logging.error(f"HuggingFace API error: {e}. Falling back to SpaCy.") print(f"HuggingFace API error: {e}. Falling back to SpaCy.") except Exception as e: logging.error(f"An error occurred while processing with Mistral: {e}. Falling back to SpaCy.") print(f"An error occurred while processing with Mistral: {e}. Falling back to SpaCy.") # Fallback to SpaCy if Mistral fails logging.warning("Mistral failed, switching to SpaCy.") print("------Spacy-----") return Parser_from_model(file_path) # ///////////////////////////////////////////// # ////////////////Spacy.py///////////////////// # ///////////////////////////////////////////// import spacy from spacy.training import Example from spacy.util import minibatch, compounding from pathlib import Path from spacy.tokens import DocBin import random # Load the training data from the .spacy file def load_data_from_spacy_file(file_path): # Initialize a blank English model to ensure compatibility nlp = spacy.blank("en") # Load the DocBin object and get documents try: doc_bin = DocBin().from_disk(file_path) docs = list(doc_bin.get_docs(nlp.vocab)) return docs except Exception as e: print(f"Error loading data from .spacy file: {e}") return [] # Train model function def train_model(epochs, model_path): # Initialize a blank English model nlp = spacy.blank("en") # Create an NER component and add it to the pipeline if "ner" not in nlp.pipe_names: ner = nlp.add_pipe("ner") nlp.add_pipe("sentencizer") # Define all possible entity labels labels = [ "PERSON", "CONTACT", "EMAIL", "ABOUT", "EXPERIENCE", "YEARS_EXPERIENCE", "UNIVERSITY", "SOFT_SKILL", "INSTITUTE", "LAST_QUALIFICATION_YEAR", "JOB_TITLE", "COMPANY", "COURSE", "DOB", "HOBBIES", "LINK", "SCHOOL", "QUALIFICATION", "LANGUAGE", "LOCATION", "PROJECTS", "SKILL", "CERTIFICATE" ] # Add labels to the NER component for label in labels: ner.add_label(label) # Load the training data train_data = load_data_from_spacy_file("./data/Spacy_data.spacy") # Start the training optimizer = nlp.begin_training() epoch_losses = [] best_loss = float('inf') # Training loop for epoch in range(epochs): losses = {} random.shuffle(train_data) # Shuffle data for better training # Create minibatches batches = minibatch(train_data, size=compounding(4.0, 32.0, 1.001)) for batch in batches: texts, annotations = zip(*[(doc.text, {"entities": [(ent.start_char, ent.end_char, ent.label_) for ent in doc.ents]}) for doc in batch]) # Convert to Example objects examples = [Example.from_dict(nlp.make_doc(text), annotation) for text, annotation in zip(texts, annotations)] # Update the model nlp.update(examples, sgd=optimizer, drop=0.35, losses=losses) current_loss = losses.get("ner", float('inf')) epoch_losses.append(current_loss) print(f"Losses at epoch {epoch + 1}: {losses}") # Stop training if the loss is zero if current_loss == 0: break # Save the best model if current_loss < best_loss: best_loss = current_loss nlp.to_disk(model_path) # Save the final model nlp.to_disk(model_path) return epoch_losses