Ahmed-Alghamdi commited on
Commit
1c43fcc
·
verified ·
1 Parent(s): 1a20432

Update response_generator.py

Browse files
Files changed (1) hide show
  1. response_generator.py +174 -80
response_generator.py CHANGED
@@ -8,120 +8,214 @@ logger = setup_logger('response_generator')
8
 
9
  class ResponseGenerator:
10
  def __init__(self):
11
- # Use a simpler approach with a summarization/QA pipeline
12
- # Since BERT-based models don't generate text, we'll create a simple retrieval-based response
13
- self.tokenizer = AutoTokenizer.from_pretrained(Config.LLM_MODEL)
14
- logger.info(f"Tokenizer loaded from {Config.LLM_MODEL}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  def generate_response(self, query, relevant_docs):
 
 
 
17
  try:
18
  if len(relevant_docs) == 0:
19
  return "عذرًا، لم أجد أي معلومات ذات صلة في المستندات."
20
 
21
- # NEW: Use multi-chunk synthesis for better answers
22
- response = self._create_multi_chunk_answer(query, relevant_docs)
23
- return response
 
 
 
24
 
25
  except Exception as e:
26
  logger.error(f"Error generating response: {e}")
27
  return "عذرًا، لم أتمكن من إنشاء استجابة بسبب خطأ ما."
28
 
29
- def _prepare_context(self, relevant_docs):
30
- # Take only the top 3 most relevant documents to avoid token limit
31
- top_docs = relevant_docs.head(3)
32
- combined_content = "\n\n".join(top_docs['content'].tolist())
33
-
34
- # Limit to 300 characters to stay within token limits
35
- max_context_length = 300
36
- return combined_content[:max_context_length]
37
-
38
- # NEW: Multi-chunk answer synthesis
39
- def _create_multi_chunk_answer(self, query, relevant_docs):
40
  """
41
- Create a comprehensive answer by combining top relevant chunks
42
  """
43
- # Get top 3 chunks
44
  top_chunks = relevant_docs.head(3)
45
 
46
- # Build answer
47
- answer_parts = []
48
- answer_parts.append("**بناءً على المستندات المتاحة:**\n")
 
49
 
50
- for idx, (_, row) in enumerate(top_chunks.iterrows(), 1):
51
- content = row['content'].strip()
52
- similarity = row.get('similarity_score', 0)
53
-
54
- # Add chunk with similarity indicator
55
- if len(top_chunks) > 1:
56
- # NEW: Show similarity score for transparency
57
- answer_parts.append(f"\n**[{idx}] مطابقة: {similarity:.0%}**")
58
-
59
- answer_parts.append(content)
60
 
61
- # NEW: Add metadata footer
62
- answer_parts.append("\n\n---")
 
63
 
64
- # NEW: Show number of sources
65
- unique_files = top_chunks['path'].nunique()
66
- answer_parts.append(f"**عدد المصادر:** {unique_files}")
 
 
 
67
 
68
- # NEW: Show score range for quality indicator
69
- if 'similarity_score' in top_chunks.columns:
70
- scores = top_chunks['similarity_score'].values
71
- if len(scores) == 1:
72
- answer_parts.append(f" | **المطابقة:** {scores[0]:.0%}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  else:
74
- answer_parts.append(f" | **المطابقة:** {scores[-1]:.0%} - {scores[0]:.0%}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- return "\n".join(answer_parts)
77
 
78
- def _create_extractive_answer(self, query, context, relevant_docs):
79
  """
80
- Create an answer by extracting relevant information from documents
81
  """
82
- # Get the most relevant document
83
- most_relevant = relevant_docs.iloc[0]['content']
84
 
85
- # Truncate to reasonable length
86
- max_length = 500
87
- if len(most_relevant) > max_length:
88
- # Try to find a good sentence break
89
- truncated = most_relevant[:max_length]
90
- last_period = truncated.rfind('.')
91
- if last_period > 0:
92
- most_relevant = truncated[:last_period + 1]
93
- else:
94
- most_relevant = truncated + "..."
95
 
96
- # NEW: Include similarity score if available
97
- similarity_info = ""
98
- if 'similarity_score' in relevant_docs.columns:
99
- score = relevant_docs.iloc[0]['similarity_score']
100
- similarity_info = f"\n**المطابقة:** {score:.0%}"
 
 
 
 
 
 
 
101
 
102
- # Format the response
103
- response = f"""بناءً على المستندات المتاحة:
 
 
 
 
 
 
 
 
 
104
 
105
- {most_relevant}
106
 
107
  ---
108
- المصدر: {relevant_docs.iloc[0]['path']}{similarity_info}"""
 
 
109
 
110
- return response
111
-
112
- def _create_prompt(self, query, context):
113
- return f"""مستند قانوني:
114
- {context}
115
-
116
- سؤال:
117
- {query}
118
-
119
- إجابة:"""
120
 
121
  def update_model(self, new_model_name):
122
  try:
123
  self.tokenizer = AutoTokenizer.from_pretrained(new_model_name)
 
 
 
 
124
  logger.info(f"Model updated to {new_model_name}")
125
  except Exception as e:
126
- logger.error(f"Error updating model: {e}")
127
-
 
8
 
9
  class ResponseGenerator:
10
  def __init__(self):
11
+ """
12
+ Initialize Arabic LLM for smart generation
13
+ """
14
+ # Use AraGPT2 for Arabic text generation
15
+ model_name = "aubmindlab/aragpt2-base" # Free Arabic GPT model
16
+
17
+ try:
18
+ logger.info(f"Loading Arabic generation model: {model_name}")
19
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
20
+ self.model = AutoModelForCausalLM.from_pretrained(model_name)
21
+
22
+ # Set pad token
23
+ if self.tokenizer.pad_token is None:
24
+ self.tokenizer.pad_token = self.tokenizer.eos_token
25
+
26
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
27
+ self.model.to(self.device)
28
+ logger.info(f"Model loaded successfully on {self.device}")
29
+ self.use_generation = True
30
+
31
+ except Exception as e:
32
+ logger.warning(f"Could not load generation model: {e}")
33
+ self.use_generation = False
34
+ self.tokenizer = None
35
+ self.model = None
36
 
37
  def generate_response(self, query, relevant_docs):
38
+ """
39
+ Generate intelligent answer from documents
40
+ """
41
  try:
42
  if len(relevant_docs) == 0:
43
  return "عذرًا، لم أجد أي معلومات ذات صلة في المستندات."
44
 
45
+ # Use smart generation if model available
46
+ if self.use_generation and self.model is not None:
47
+ return self._generate_smart_answer(query, relevant_docs)
48
+ else:
49
+ # Fallback: smart extraction
50
+ return self._generate_extractive_answer(query, relevant_docs)
51
 
52
  except Exception as e:
53
  logger.error(f"Error generating response: {e}")
54
  return "عذرًا، لم أتمكن من إنشاء استجابة بسبب خطأ ما."
55
 
56
+ def _generate_smart_answer(self, query, relevant_docs):
 
 
 
 
 
 
 
 
 
 
57
  """
58
+ Generate smart answer using LLM
59
  """
60
+ # Get top 3 most relevant chunks
61
  top_chunks = relevant_docs.head(3)
62
 
63
+ # Combine context
64
+ contexts = []
65
+ for idx, row in top_chunks.iterrows():
66
+ contexts.append(row['content'][:200]) # Limit each chunk to 200 chars
67
 
68
+ combined_context = " ".join(contexts)
 
 
 
 
 
 
 
 
 
69
 
70
+ # Limit total context
71
+ if len(combined_context) > 400:
72
+ combined_context = combined_context[:400]
73
 
74
+ # Create prompt for generation
75
+ prompt = f"""النص: {combined_context}
76
+
77
+ السؤال: {query}
78
+
79
+ الإجابة المختصرة:"""
80
 
81
+ try:
82
+ # Tokenize
83
+ inputs = self.tokenizer(
84
+ prompt,
85
+ return_tensors="pt",
86
+ max_length=512,
87
+ truncation=True,
88
+ padding=True
89
+ ).to(self.device)
90
+
91
+ # Generate answer
92
+ with torch.no_grad():
93
+ outputs = self.model.generate(
94
+ inputs['input_ids'],
95
+ attention_mask=inputs['attention_mask'],
96
+ max_new_tokens=100, # Generate max 100 new tokens
97
+ min_length=20, # Minimum length
98
+ num_beams=3, # Beam search for better quality
99
+ no_repeat_ngram_size=3, # Avoid repetition
100
+ do_sample=True, # Sampling for diversity
101
+ top_k=50,
102
+ top_p=0.9,
103
+ temperature=0.7,
104
+ pad_token_id=self.tokenizer.pad_token_id,
105
+ eos_token_id=self.tokenizer.eos_token_id
106
+ )
107
+
108
+ # Decode the answer
109
+ full_response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
110
+
111
+ # Extract only the generated answer (remove the prompt)
112
+ if "الإجابة المختصرة:" in full_response:
113
+ answer = full_response.split("الإجابة المختصرة:")[-1].strip()
114
  else:
115
+ answer = full_response[len(prompt):].strip()
116
+
117
+ # Clean up the answer
118
+ answer = self._clean_answer(answer)
119
+
120
+ # Add metadata
121
+ scores = top_chunks['similarity_score'].values if 'similarity_score' in top_chunks.columns else []
122
+
123
+ response = f"""**الإجابة:**
124
+
125
+ {answer}
126
+
127
+ ---
128
+ **عدد المصادر:** {len(top_chunks)} chunks"""
129
+
130
+ if len(scores) > 0:
131
+ response += f" | **دقة المطابقة:** {scores[-1]:.0%} - {scores[0]:.0%}"
132
+
133
+ logger.info(f"Generated answer length: {len(answer)} chars")
134
+
135
+ return response
136
+
137
+ except Exception as e:
138
+ logger.error(f"Generation failed: {e}")
139
+ # Fallback to extractive method
140
+ return self._generate_extractive_answer(query, relevant_docs)
141
+
142
+ def _clean_answer(self, answer):
143
+ """
144
+ Clean and format the generated answer
145
+ """
146
+ # Remove incomplete sentences at the end
147
+ if answer and not answer.endswith(('.', '؟', '!')):
148
+ # Find last complete sentence
149
+ last_period = max(answer.rfind('.'), answer.rfind('؟'), answer.rfind('!'))
150
+ if last_period > 20: # Keep if reasonable length
151
+ answer = answer[:last_period + 1]
152
+
153
+ # Remove extra whitespace
154
+ answer = ' '.join(answer.split())
155
+
156
+ # Limit length
157
+ if len(answer) > 500:
158
+ answer = answer[:500]
159
+ # Try to end at sentence
160
+ last_period = max(answer.rfind('.'), answer.rfind('؟'))
161
+ if last_period > 200:
162
+ answer = answer[:last_period + 1]
163
 
164
+ return answer.strip()
165
 
166
+ def _generate_extractive_answer(self, query, relevant_docs):
167
  """
168
+ Smart extraction without generation model (fallback)
169
  """
170
+ top_chunks = relevant_docs.head(3)
 
171
 
172
+ # Analyze query to find most relevant chunk
173
+ query_lower = query.lower()
174
+
175
+ best_chunk = None
176
+ best_score = 0
 
 
 
 
 
177
 
178
+ for idx, row in top_chunks.iterrows():
179
+ content = row['content']
180
+ score = row.get('similarity_score', 0)
181
+
182
+ # Boost score if query keywords appear in content
183
+ content_lower = content.lower()
184
+ keyword_matches = sum(1 for word in query_lower.split() if len(word) > 2 and word in content_lower)
185
+ adjusted_score = score + (keyword_matches * 0.05)
186
+
187
+ if adjusted_score > best_score:
188
+ best_score = adjusted_score
189
+ best_chunk = content
190
 
191
+ # Format answer
192
+ if best_chunk:
193
+ # Truncate if too long
194
+ if len(best_chunk) > 400:
195
+ best_chunk = best_chunk[:400]
196
+ # Find sentence end
197
+ last_period = max(best_chunk.rfind('.'), best_chunk.rfind('؟'))
198
+ if last_period > 100:
199
+ best_chunk = best_chunk[:last_period + 1]
200
+
201
+ response = f"""**الإجابة:**
202
 
203
+ {best_chunk}
204
 
205
  ---
206
+ **المصدر:** {len(top_chunks)} chunks | **المطابقة:** {best_score:.0%}"""
207
+
208
+ return response
209
 
210
+ return "عذرًا، لم أتمكن من إيجاد إجابة مناسبة."
 
 
 
 
 
 
 
 
 
211
 
212
  def update_model(self, new_model_name):
213
  try:
214
  self.tokenizer = AutoTokenizer.from_pretrained(new_model_name)
215
+ self.model = AutoModelForCausalLM.from_pretrained(new_model_name)
216
+ if self.tokenizer.pad_token is None:
217
+ self.tokenizer.pad_token = self.tokenizer.eos_token
218
+ self.model.to(self.device)
219
  logger.info(f"Model updated to {new_model_name}")
220
  except Exception as e:
221
+ logger.error(f"Error updating model: {e}")