|
|
import gradio as gr |
|
|
import torch |
|
|
import spaces |
|
|
|
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig |
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter |
|
|
from langchain_community.vectorstores import FAISS |
|
|
from langchain_community.embeddings import HuggingFaceEmbeddings |
|
|
|
|
|
import PyPDF2 |
|
|
from docx import Document |
|
|
|
|
|
|
|
|
class ResumeRAG: |
|
|
def __init__(self): |
|
|
self.has_cuda = torch.cuda.is_available() |
|
|
self.device = "cuda" if self.has_cuda else "cpu" |
|
|
print(f"Using device: {self.device}") |
|
|
|
|
|
|
|
|
self.embeddings = HuggingFaceEmbeddings( |
|
|
model_name="sentence-transformers/all-MiniLM-L6-v2", |
|
|
model_kwargs={"device": self.device}, |
|
|
) |
|
|
|
|
|
self.text_splitter = RecursiveCharacterTextSplitter( |
|
|
chunk_size=500, |
|
|
chunk_overlap=50 |
|
|
) |
|
|
|
|
|
self.vector_store = None |
|
|
|
|
|
model_name = "mistralai/Mistral-7B-Instruct-v0.2" |
|
|
|
|
|
if not self.has_cuda: |
|
|
raise RuntimeError( |
|
|
"No CUDA GPU detected. Use a GPU Space/ZeroGPU, or switch to a smaller CPU model." |
|
|
) |
|
|
|
|
|
|
|
|
quantization_config = BitsAndBytesConfig( |
|
|
load_in_4bit=True, |
|
|
bnb_4bit_compute_dtype=torch.float16, |
|
|
bnb_4bit_use_double_quant=True, |
|
|
bnb_4bit_quant_type="nf4", |
|
|
) |
|
|
|
|
|
print("Loading tokenizer...") |
|
|
self.tokenizer = AutoTokenizer.from_pretrained(model_name) |
|
|
|
|
|
print("Loading model...") |
|
|
self.model = AutoModelForCausalLM.from_pretrained( |
|
|
model_name, |
|
|
quantization_config=quantization_config, |
|
|
device_map="auto", |
|
|
trust_remote_code=True |
|
|
) |
|
|
|
|
|
|
|
|
if self.tokenizer.pad_token_id is None: |
|
|
self.tokenizer.pad_token = self.tokenizer.eos_token |
|
|
|
|
|
def extract_text_from_pdf(self, file_path: str) -> str: |
|
|
try: |
|
|
with open(file_path, "rb") as f: |
|
|
reader = PyPDF2.PdfReader(f) |
|
|
return "".join([(p.extract_text() or "") for p in reader.pages]) |
|
|
except Exception as e: |
|
|
return f"Error reading PDF: {e}" |
|
|
|
|
|
def extract_text_from_docx(self, file_path: str) -> str: |
|
|
try: |
|
|
doc = Document(file_path) |
|
|
return "\n".join([p.text for p in doc.paragraphs]) |
|
|
except Exception as e: |
|
|
return f"Error reading DOCX: {e}" |
|
|
|
|
|
def process_resume(self, file) -> str: |
|
|
if file is None: |
|
|
return "Please upload a resume file." |
|
|
|
|
|
file_path = file.name |
|
|
if file_path.lower().endswith(".pdf"): |
|
|
text = self.extract_text_from_pdf(file_path) |
|
|
elif file_path.lower().endswith(".docx"): |
|
|
text = self.extract_text_from_docx(file_path) |
|
|
else: |
|
|
return "Unsupported file format. Please upload PDF or DOCX." |
|
|
|
|
|
if text.startswith("Error"): |
|
|
return text |
|
|
|
|
|
if not text.strip(): |
|
|
return "No text could be extracted from the resume." |
|
|
|
|
|
chunks = self.text_splitter.split_text(text) |
|
|
if not chunks: |
|
|
return "No text chunks could be created from the resume." |
|
|
|
|
|
self.vector_store = FAISS.from_texts(chunks, self.embeddings) |
|
|
return f"β
Resume processed successfully! Extracted {len(chunks)} text chunks." |
|
|
|
|
|
def generate_answer(self, question: str, context: str) -> str: |
|
|
prompt = f"""[INST] You are a helpful assistant analyzing a resume. |
|
|
|
|
|
Context: |
|
|
{context} |
|
|
|
|
|
Question: {question} |
|
|
|
|
|
Answer only from the context. If the answer is not in the context, say it is not in the resume. [/INST]""" |
|
|
|
|
|
inputs = self.tokenizer(prompt, return_tensors="pt") |
|
|
|
|
|
|
|
|
target_device = self.model.get_input_embeddings().weight.device |
|
|
inputs = {k: v.to(target_device) for k, v in inputs.items()} |
|
|
|
|
|
with torch.no_grad(): |
|
|
outputs = self.model.generate( |
|
|
**inputs, |
|
|
max_new_tokens=1024, |
|
|
temperature=0.7, |
|
|
top_p=0.9, |
|
|
do_sample=True, |
|
|
pad_token_id=self.tokenizer.eos_token_id, |
|
|
) |
|
|
|
|
|
text = self.tokenizer.decode(outputs[0], skip_special_tokens=True) |
|
|
|
|
|
|
|
|
if "[/INST]" in text: |
|
|
return text.split("[/INST]")[-1].strip() |
|
|
return text.strip() |
|
|
|
|
|
def query(self, question: str): |
|
|
if self.vector_store is None: |
|
|
return "Please upload a resume first.", "" |
|
|
|
|
|
if not question.strip(): |
|
|
return "Please enter a question.", "" |
|
|
|
|
|
docs = self.vector_store.similarity_search(question, k=3) |
|
|
context = "\n\n".join([d.page_content for d in docs]) |
|
|
|
|
|
answer = self.generate_answer(question, context) |
|
|
|
|
|
if torch.cuda.is_available(): |
|
|
torch.cuda.empty_cache() |
|
|
|
|
|
return answer, context |
|
|
|
|
|
|
|
|
print("Initializing Resume RAG System...") |
|
|
rag_system = ResumeRAG() |
|
|
|
|
|
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo: |
|
|
gr.Markdown( |
|
|
""" |
|
|
# π Resume RAG Q&A System |
|
|
Powered by Mistral-7B + FAISS vector search |
|
|
|
|
|
Upload your resume and ask questions about experience, skills, education, and more. |
|
|
""" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### π€ Upload Resume") |
|
|
file_input = gr.File( |
|
|
label="Upload PDF or DOCX", |
|
|
file_types=[".pdf", ".docx"] |
|
|
) |
|
|
upload_btn = gr.Button("Process Resume", variant="primary", size="lg") |
|
|
upload_status = gr.Textbox(label="Status", interactive=False) |
|
|
|
|
|
gr.Markdown( |
|
|
""" |
|
|
--- |
|
|
**Example Questions:** |
|
|
- What programming languages does the candidate know? |
|
|
- Summarize the work experience |
|
|
- What is the education background? |
|
|
- List all technical skills |
|
|
""" |
|
|
) |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown("### π¬ Ask Questions") |
|
|
question_input = gr.Textbox( |
|
|
label="Your Question", |
|
|
placeholder="e.g., What are the candidate's key skills?", |
|
|
lines=2 |
|
|
) |
|
|
submit_btn = gr.Button("Get Answer", variant="primary", size="lg") |
|
|
|
|
|
answer_output = gr.Textbox( |
|
|
label="Answer", |
|
|
lines=8, |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
with gr.Accordion("π Retrieved Context", open=False): |
|
|
context_output = gr.Textbox( |
|
|
label="Relevant Resume Sections", |
|
|
lines=6, |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
|
|
|
@spaces.GPU |
|
|
def query_gpu(q): |
|
|
return rag_system.query(q) |
|
|
|
|
|
upload_btn.click( |
|
|
fn=rag_system.process_resume, |
|
|
inputs=[file_input], |
|
|
outputs=[upload_status] |
|
|
) |
|
|
|
|
|
submit_btn.click( |
|
|
fn=query_gpu, |
|
|
inputs=[question_input], |
|
|
outputs=[answer_output, context_output] |
|
|
) |
|
|
|
|
|
question_input.submit( |
|
|
fn=query_gpu, |
|
|
inputs=[question_input], |
|
|
outputs=[answer_output, context_output] |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(share=True) |
|
|
|