telcom commited on
Commit
bebc177
·
verified ·
1 Parent(s): 9d229d6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -151
app.py CHANGED
@@ -1,114 +1,88 @@
1
  import gradio as gr
2
  import torch
 
3
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
4
- from sentence_transformers import SentenceTransformer
5
  from langchain_text_splitters import RecursiveCharacterTextSplitter
6
  from langchain_community.vectorstores import FAISS
7
  from langchain_community.embeddings import HuggingFaceEmbeddings
8
  import PyPDF2
9
  from docx import Document
10
- import numpy as np
11
- from typing import List, Tuple
12
- import gc
13
 
14
  class ResumeRAG:
15
  def __init__(self):
16
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
 
17
  print(f"Using device: {self.device}")
18
-
19
- # Initialize embedding model (lightweight)
20
  self.embeddings = HuggingFaceEmbeddings(
21
  model_name="sentence-transformers/all-MiniLM-L6-v2",
22
- model_kwargs={'device': self.device}
23
  )
24
-
25
- # Initialize LLM with 4-bit quantization for GPU efficiency
 
 
 
 
26
  quantization_config = BitsAndBytesConfig(
27
  load_in_4bit=True,
28
  bnb_4bit_compute_dtype=torch.float16,
29
  bnb_4bit_use_double_quant=True,
30
- bnb_4bit_quant_type="nf4"
31
  )
32
-
33
- model_name = "mistralai/Mistral-7B-Instruct-v0.2"
34
-
35
  print("Loading model...")
36
  self.tokenizer = AutoTokenizer.from_pretrained(model_name)
37
  self.model = AutoModelForCausalLM.from_pretrained(
38
  model_name,
39
  quantization_config=quantization_config,
40
  device_map="auto",
41
- trust_remote_code=True
42
  )
43
-
44
  self.vector_store = None
45
- self.text_splitter = RecursiveCharacterTextSplitter(
46
- chunk_size=500,
47
- chunk_overlap=50
48
- )
49
-
50
  def extract_text_from_pdf(self, file_path: str) -> str:
51
- """Extract text from PDF file"""
52
- try:
53
- with open(file_path, 'rb') as file:
54
- pdf_reader = PyPDF2.PdfReader(file)
55
- text = ""
56
- for page in pdf_reader.pages:
57
- text += page.extract_text()
58
- return text
59
- except Exception as e:
60
- return f"Error reading PDF: {str(e)}"
61
-
62
  def extract_text_from_docx(self, file_path: str) -> str:
63
- """Extract text from DOCX file"""
64
- try:
65
- doc = Document(file_path)
66
- text = "\n".join([paragraph.text for paragraph in doc.paragraphs])
67
- return text
68
- except Exception as e:
69
- return f"Error reading DOCX: {str(e)}"
70
-
71
  def process_resume(self, file) -> str:
72
- """Process uploaded resume and create vector store"""
73
  if file is None:
74
  return "Please upload a resume file."
75
-
76
- # Extract text based on file type
77
  file_path = file.name
78
- if file_path.endswith('.pdf'):
79
  text = self.extract_text_from_pdf(file_path)
80
- elif file_path.endswith('.docx'):
81
  text = self.extract_text_from_docx(file_path)
82
  else:
83
  return "Unsupported file format. Please upload PDF or DOCX."
84
-
85
- if text.startswith("Error"):
86
- return text
87
-
88
- # Split text into chunks
89
- chunks = self.text_splitter.split_text(text)
90
-
91
- if not chunks:
92
  return "No text could be extracted from the resume."
93
-
94
- # Create vector store
95
  self.vector_store = FAISS.from_texts(chunks, self.embeddings)
96
-
97
- return f"✅ Resume processed successfully! Extracted {len(chunks)} text chunks. You can now ask questions."
98
-
99
  def generate_answer(self, question: str, context: str) -> str:
100
- """Generate answer using LLM"""
101
- prompt = f"""[INST] You are a helpful assistant analyzing a resume. Use the following context to answer the question accurately and concisely.
102
 
103
- Context from resume:
104
  {context}
105
 
106
  Question: {question}
107
 
108
- Provide a clear, specific answer based only on the information in the context. If the information is not in the context, say so. [/INST]"""
109
-
110
- inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
111
-
112
  with torch.no_grad():
113
  outputs = self.model.generate(
114
  **inputs,
@@ -116,108 +90,51 @@ Provide a clear, specific answer based only on the information in the context. I
116
  temperature=0.7,
117
  top_p=0.9,
118
  do_sample=True,
119
- pad_token_id=self.tokenizer.eos_token_id
120
  )
121
-
122
- answer = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
123
- # Extract only the assistant's response
124
- answer = answer.split("[/INST]")[-1].strip()
125
-
126
- return answer
127
-
128
- def query(self, question: str) -> Tuple[str, str]:
129
- """Query the RAG system"""
130
  if self.vector_store is None:
131
  return "Please upload a resume first.", ""
132
-
133
  if not question.strip():
134
  return "Please enter a question.", ""
135
-
136
- # Retrieve relevant chunks
137
  docs = self.vector_store.similarity_search(question, k=3)
138
- context = "\n\n".join([doc.page_content for doc in docs])
139
-
140
- # Generate answer
141
  answer = self.generate_answer(question, context)
142
-
143
- # Clear cache to manage GPU memory
144
- if self.device == "cuda":
145
- torch.cuda.empty_cache()
146
-
147
  return answer, context
148
 
149
- # Initialize RAG system
150
- print("Initializing Resume RAG System...")
151
  rag_system = ResumeRAG()
152
 
153
- # Create Gradio interface
154
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
155
- gr.Markdown("""
156
- # 📄 Resume RAG Q&A System
157
- ### Powered by Mistral-7B + FAISS Vector Search
158
-
159
- Upload your resume and ask questions about experience, skills, education, and more!
160
- """)
161
-
162
  with gr.Row():
163
  with gr.Column(scale=1):
164
- gr.Markdown("### 📤 Upload Resume")
165
- file_input = gr.File(
166
- label="Upload PDF or DOCX",
167
- file_types=[".pdf", ".docx"]
168
- )
169
- upload_btn = gr.Button("Process Resume", variant="primary", size="lg")
170
  upload_status = gr.Textbox(label="Status", interactive=False)
171
-
172
- gr.Markdown("""
173
- ---
174
- **Example Questions:**
175
- - What programming languages does the candidate know?
176
- - Summarize the work experience
177
- - What is the candidate's education background?
178
- - List all technical skills
179
- """)
180
-
181
  with gr.Column(scale=2):
182
- gr.Markdown("### 💬 Ask Questions")
183
- question_input = gr.Textbox(
184
- label="Your Question",
185
- placeholder="e.g., What are the candidate's key skills?",
186
- lines=2
187
- )
188
- submit_btn = gr.Button("Get Answer", variant="primary", size="lg")
189
-
190
- answer_output = gr.Textbox(
191
- label="Answer",
192
- lines=8,
193
- interactive=False
194
- )
195
-
196
  with gr.Accordion("📚 Retrieved Context", open=False):
197
- context_output = gr.Textbox(
198
- label="Relevant Resume Sections",
199
- lines=6,
200
- interactive=False
201
- )
202
-
203
- # Event handlers
204
- upload_btn.click(
205
- fn=rag_system.process_resume,
206
- inputs=[file_input],
207
- outputs=[upload_status]
208
- )
209
-
210
- submit_btn.click(
211
- fn=rag_system.query,
212
- inputs=[question_input],
213
- outputs=[answer_output, context_output]
214
- )
215
-
216
- question_input.submit(
217
- fn=rag_system.query,
218
- inputs=[question_input],
219
- outputs=[answer_output, context_output]
220
- )
221
 
222
  if __name__ == "__main__":
223
- demo.launch(share=False)
 
1
  import gradio as gr
2
  import torch
3
+ import spaces
4
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
 
5
  from langchain_text_splitters import RecursiveCharacterTextSplitter
6
  from langchain_community.vectorstores import FAISS
7
  from langchain_community.embeddings import HuggingFaceEmbeddings
8
  import PyPDF2
9
  from docx import Document
 
 
 
10
 
11
  class ResumeRAG:
12
  def __init__(self):
13
+ self.has_cuda = torch.cuda.is_available()
14
+ self.device = "cuda" if self.has_cuda else "cpu"
15
  print(f"Using device: {self.device}")
16
+
 
17
  self.embeddings = HuggingFaceEmbeddings(
18
  model_name="sentence-transformers/all-MiniLM-L6-v2",
19
+ model_kwargs={"device": self.device},
20
  )
21
+
22
+ model_name = "mistralai/Mistral-7B-Instruct-v0.2"
23
+
24
+ if not self.has_cuda:
25
+ raise RuntimeError("GPU not available. Set Space hardware to GPU or use the CPU fallback option.")
26
+
27
  quantization_config = BitsAndBytesConfig(
28
  load_in_4bit=True,
29
  bnb_4bit_compute_dtype=torch.float16,
30
  bnb_4bit_use_double_quant=True,
31
+ bnb_4bit_quant_type="nf4",
32
  )
33
+
 
 
34
  print("Loading model...")
35
  self.tokenizer = AutoTokenizer.from_pretrained(model_name)
36
  self.model = AutoModelForCausalLM.from_pretrained(
37
  model_name,
38
  quantization_config=quantization_config,
39
  device_map="auto",
40
+ trust_remote_code=True,
41
  )
42
+
43
  self.vector_store = None
44
+ self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
45
+
 
 
 
46
  def extract_text_from_pdf(self, file_path: str) -> str:
47
+ with open(file_path, "rb") as f:
48
+ reader = PyPDF2.PdfReader(f)
49
+ return "".join([(p.extract_text() or "") for p in reader.pages])
50
+
 
 
 
 
 
 
 
51
  def extract_text_from_docx(self, file_path: str) -> str:
52
+ doc = Document(file_path)
53
+ return "\n".join([p.text for p in doc.paragraphs])
54
+
 
 
 
 
 
55
  def process_resume(self, file) -> str:
 
56
  if file is None:
57
  return "Please upload a resume file."
58
+
 
59
  file_path = file.name
60
+ if file_path.endswith(".pdf"):
61
  text = self.extract_text_from_pdf(file_path)
62
+ elif file_path.endswith(".docx"):
63
  text = self.extract_text_from_docx(file_path)
64
  else:
65
  return "Unsupported file format. Please upload PDF or DOCX."
66
+
67
+ if not text.strip():
 
 
 
 
 
 
68
  return "No text could be extracted from the resume."
69
+
70
+ chunks = self.text_splitter.split_text(text)
71
  self.vector_store = FAISS.from_texts(chunks, self.embeddings)
72
+ return f"✅ Resume processed successfully! Extracted {len(chunks)} text chunks."
73
+
 
74
  def generate_answer(self, question: str, context: str) -> str:
75
+ prompt = f"""[INST] You are a helpful assistant analyzing a resume.
 
76
 
77
+ Context:
78
  {context}
79
 
80
  Question: {question}
81
 
82
+ Answer only from the context. If missing, say it is not in the resume. [/INST]"""
83
+
84
+ # IMPORTANT: do NOT push inputs to self.device when device_map="auto"
85
+ inputs = self.tokenizer(prompt, return_tensors="pt")
86
  with torch.no_grad():
87
  outputs = self.model.generate(
88
  **inputs,
 
90
  temperature=0.7,
91
  top_p=0.9,
92
  do_sample=True,
93
+ pad_token_id=self.tokenizer.eos_token_id,
94
  )
95
+
96
+ text = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
97
+ return text.split("[/INST]")[-1].strip()
98
+
99
+ def query(self, question: str):
 
 
 
 
100
  if self.vector_store is None:
101
  return "Please upload a resume first.", ""
 
102
  if not question.strip():
103
  return "Please enter a question.", ""
104
+
 
105
  docs = self.vector_store.similarity_search(question, k=3)
106
+ context = "\n\n".join([d.page_content for d in docs])
 
 
107
  answer = self.generate_answer(question, context)
108
+
109
+ torch.cuda.empty_cache()
 
 
 
110
  return answer, context
111
 
 
 
112
  rag_system = ResumeRAG()
113
 
 
114
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
115
+ gr.Markdown("# 📄 Resume RAG Q&A System")
116
+
 
 
 
 
 
117
  with gr.Row():
118
  with gr.Column(scale=1):
119
+ file_input = gr.File(label="Upload PDF or DOCX", file_types=[".pdf", ".docx"])
120
+ upload_btn = gr.Button("Process Resume", variant="primary")
 
 
 
 
121
  upload_status = gr.Textbox(label="Status", interactive=False)
122
+
 
 
 
 
 
 
 
 
 
123
  with gr.Column(scale=2):
124
+ question_input = gr.Textbox(label="Your Question", lines=2)
125
+ submit_btn = gr.Button("Get Answer", variant="primary")
126
+ answer_output = gr.Textbox(label="Answer", lines=8, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
127
  with gr.Accordion("📚 Retrieved Context", open=False):
128
+ context_output = gr.Textbox(label="Relevant Resume Sections", lines=6, interactive=False)
129
+
130
+ # Wrap the callback so Spaces sees a GPU-decorated function
131
+ @spaces.GPU
132
+ def query_gpu(q):
133
+ return rag_system.query(q)
134
+
135
+ upload_btn.click(rag_system.process_resume, inputs=[file_input], outputs=[upload_status])
136
+ submit_btn.click(query_gpu, inputs=[question_input], outputs=[answer_output, context_output])
137
+ question_input.submit(query_gpu, inputs=[question_input], outputs=[answer_output, context_output])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  if __name__ == "__main__":
140
+ demo.launch()