sharshar1 commited on
Commit
73d0f5f
·
verified ·
1 Parent(s): d4e2343

Update RAG to return sources

Browse files
Files changed (1) hide show
  1. rag_chain.py +390 -0
rag_chain.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ نظام RAG للـ HR Chatbot باستخدام Ollama و Llama 3.1
3
+ HR Chatbot RAG System with Ollama & Llama 3.1
4
+
5
+ هذه النسخة تستخدم:
6
+ - sentence-transformers للـ Embeddings
7
+ - FAISS للـ Vector Store
8
+ - Ollama API مباشرة للـ LLM
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import requests
14
+ import numpy as np
15
+ from typing import List, Dict, Optional, Tuple
16
+
17
+
18
+ class Document:
19
+ """مستند بسيط للـ RAG"""
20
+ def __init__(self, page_content: str, metadata: dict = None):
21
+ self.page_content = page_content
22
+ self.metadata = metadata or {}
23
+
24
+
25
+ class SimpleVectorStore:
26
+ """Vector Store بسيط باستخدام FAISS"""
27
+
28
+ def __init__(self, embedding_model):
29
+ self.embedding_model = embedding_model
30
+ self.documents: List[Document] = []
31
+ self.embeddings: Optional[np.ndarray] = None
32
+ self.index = None
33
+
34
+ def add_documents(self, documents: List[Document]):
35
+ """إضافة مستندات"""
36
+ # استخراج النصوص
37
+ texts = [doc.page_content for doc in documents]
38
+
39
+ # إنشاء embeddings
40
+ new_embeddings = self.embedding_model.encode(texts, convert_to_numpy=True)
41
+
42
+ if self.embeddings is None:
43
+ self.embeddings = new_embeddings
44
+ self.documents = documents
45
+ else:
46
+ self.embeddings = np.vstack([self.embeddings, new_embeddings])
47
+ self.documents.extend(documents)
48
+
49
+ # بناء FAISS index
50
+ self._build_index()
51
+
52
+ def _build_index(self):
53
+ """بناء FAISS index"""
54
+ import faiss
55
+
56
+ if self.embeddings is None:
57
+ return
58
+
59
+ # تطبيع الـ embeddings
60
+ embeddings_copy = self.embeddings.copy().astype('float32')
61
+ faiss.normalize_L2(embeddings_copy)
62
+
63
+ # إنشاء index
64
+ dimension = embeddings_copy.shape[1]
65
+ self.index = faiss.IndexFlatIP(dimension) # Inner Product (Cosine Similarity)
66
+ self.index.add(embeddings_copy)
67
+
68
+ def similarity_search(self, query: str, k: int = 5) -> List[Document]:
69
+ """بحث التشابه"""
70
+ if self.index is None:
71
+ return []
72
+
73
+ import faiss
74
+
75
+ # تحويل السؤال إلى embedding
76
+ query_embedding = self.embedding_model.encode([query], convert_to_numpy=True).astype('float32')
77
+ faiss.normalize_L2(query_embedding)
78
+
79
+ # البحث
80
+ scores, indices = self.index.search(query_embedding, k)
81
+
82
+ results = []
83
+ for idx in indices[0]:
84
+ if 0 <= idx < len(self.documents):
85
+ results.append(self.documents[idx])
86
+
87
+ return results
88
+
89
+ def similarity_search_with_score(self, query: str, k: int = 5) -> List[Tuple[Document, float]]:
90
+ """بحث التشابه مع الدرجات"""
91
+ if self.index is None:
92
+ return []
93
+
94
+ import faiss
95
+
96
+ # تحويل السؤال إلى embedding
97
+ query_embedding = self.embedding_model.encode([query], convert_to_numpy=True).astype('float32')
98
+ faiss.normalize_L2(query_embedding)
99
+
100
+ # البحث
101
+ scores, indices = self.index.search(query_embedding, k)
102
+
103
+ results = []
104
+ for score, idx in zip(scores[0], indices[0]):
105
+ if 0 <= idx < len(self.documents):
106
+ results.append((self.documents[idx], float(score)))
107
+
108
+ return results
109
+
110
+ def save(self, path: str):
111
+ """حفظ Vector Store"""
112
+ import faiss
113
+
114
+ os.makedirs(path, exist_ok=True)
115
+
116
+ # حفظ FAISS index
117
+ if self.index is not None:
118
+ faiss.write_index(self.index, os.path.join(path, "index.faiss"))
119
+
120
+ # حفظ المستندات
121
+ docs_data = [{"content": doc.page_content, "metadata": doc.metadata} for doc in self.documents]
122
+ with open(os.path.join(path, "documents.json"), "w", encoding="utf-8") as f:
123
+ json.dump(docs_data, f, ensure_ascii=False, indent=2)
124
+
125
+ # حفظ embeddings
126
+ if self.embeddings is not None:
127
+ np.save(os.path.join(path, "embeddings.npy"), self.embeddings)
128
+
129
+ def load(self, path: str) -> bool:
130
+ """تحميل Vector Store"""
131
+ import faiss
132
+
133
+ try:
134
+ # تحميل FAISS index
135
+ index_path = os.path.join(path, "index.faiss")
136
+ if os.path.exists(index_path):
137
+ self.index = faiss.read_index(index_path)
138
+
139
+ # تحميل المستندات
140
+ docs_path = os.path.join(path, "documents.json")
141
+ if os.path.exists(docs_path):
142
+ with open(docs_path, "r", encoding="utf-8") as f:
143
+ docs_data = json.load(f)
144
+ self.documents = [Document(d["content"], d["metadata"]) for d in docs_data]
145
+
146
+ # تحميل embeddings
147
+ emb_path = os.path.join(path, "embeddings.npy")
148
+ if os.path.exists(emb_path):
149
+ self.embeddings = np.load(emb_path)
150
+
151
+ return True
152
+ except Exception as e:
153
+ print(f"❌ خطأ في تحميل Vector Store: {e}")
154
+ return False
155
+
156
+
157
+ class HRChatbotRAG:
158
+ """نظام RAG للـ HR Chatbot باستخدام Ollama"""
159
+
160
+ def __init__(
161
+ self,
162
+ embedding_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
163
+ llm_model: str = "llama3.1",
164
+ ollama_base_url: str = "http://localhost:11434",
165
+ temperature: float = 0.3,
166
+ streaming: bool = False
167
+ ):
168
+ """
169
+ تهيئة نظام RAG
170
+
171
+ Args:
172
+ embedding_model: نموذج الـ Embeddings
173
+ llm_model: نموذج Ollama (llama3.1)
174
+ ollama_base_url: رابط Ollama
175
+ temperature: درجة الإبداعية
176
+ streaming: تفعيل البث المباشر
177
+ """
178
+ self.embedding_model_name = embedding_model
179
+ self.llm_model = llm_model
180
+ self.ollama_base_url = ollama_base_url
181
+ self.temperature = temperature
182
+ self.streaming = streaming
183
+
184
+ print(f"🚀 جاري تهيئة نظام RAG...")
185
+ print(f" 📦 Embedding Model: {embedding_model}")
186
+ print(f" 🤖 LLM Model: {llm_model} (Ollama)")
187
+
188
+ # تهيئة Embeddings
189
+ print(" ⏳ تحميل نموذج Embeddings...")
190
+ from sentence_transformers import SentenceTransformer
191
+ self.embedding_model = SentenceTransformer(embedding_model)
192
+
193
+ # تهيئة Vector Store
194
+ self.vectorstore = SimpleVectorStore(self.embedding_model)
195
+
196
+ # التحقق من الاتصال بـ Ollama
197
+ print(" ⏳ الاتصال بـ Ollama...")
198
+ if self._test_ollama_connection():
199
+ print(f" ✅ تم الاتصال بـ Ollama بنجاح!")
200
+ else:
201
+ print(f" ⚠️ تحذير: تأكد من تشغيل Ollama!")
202
+
203
+ print("✅ تم تهيئة النظام بنجاح!")
204
+
205
+ def _test_ollama_connection(self) -> bool:
206
+ """اختبار الاتصال بـ Ollama"""
207
+ try:
208
+ response = requests.get(f"{self.ollama_base_url}/api/tags", timeout=5)
209
+ return response.status_code == 200
210
+ except:
211
+ return False
212
+
213
+ def _call_ollama(self, prompt: str) -> str:
214
+ """استدعاء Ollama API"""
215
+ try:
216
+ response = requests.post(
217
+ f"{self.ollama_base_url}/api/generate",
218
+ json={
219
+ "model": self.llm_model,
220
+ "prompt": prompt,
221
+ "stream": False,
222
+ "options": {
223
+ "temperature": self.temperature,
224
+ "num_ctx": 4096,
225
+ "num_predict": 512
226
+ }
227
+ },
228
+ timeout=120
229
+ )
230
+
231
+ if response.status_code == 200:
232
+ return response.json().get("response", "")
233
+ else:
234
+ return f"خطأ في Ollama: {response.status_code}"
235
+ except Exception as e:
236
+ return f"خطأ في الاتصال بـ Ollama: {str(e)}"
237
+
238
+ def _create_prompt(self, context: str, question: str) -> str:
239
+ """إنشاء Prompt للـ HR Chatbot"""
240
+ return f"""أنت مساعد ذكي لنظام الموارد البشرية (HR Genie). مهمتك هي الإجابة على أسئلة الموظفين بشكل دقيق ومهني باللغة العربية.
241
+
242
+ استخدم المعلومات التالية للإجابة على السؤال. إذا لم تجد الإجابة في المعلومات المتاحة، قل ذلك بوضوح واقترح التواصل مع قسم الموارد البشرية.
243
+
244
+ المعلومات المتاحة:
245
+ {context}
246
+
247
+ السؤال: {question}
248
+
249
+ التعليمات:
250
+ - أجب باللغة العربية بشكل واضح ومختصر
251
+ - إذا كان السؤال عن إجراء معين، اشرح الخطوات بوضوح
252
+ - كن ودوداً ومهنياً في الرد
253
+ - إذا لم تكن متأكداً من الإجابة، اقترح التواصل مع قسم HR
254
+
255
+ الإجابة:"""
256
+
257
+ def build_vectorstore(self, documents: List[Document]) -> None:
258
+ """
259
+ بناء Vector Store من المستندات
260
+
261
+ Args:
262
+ documents: قائمة المستندات
263
+ """
264
+ print(f"🔨 جاري بناء Vector Store من {len(documents)} مستند...")
265
+ self.vectorstore.add_documents(documents)
266
+ print(f"✅ تم بناء Vector Store بنجاح!")
267
+
268
+ def save_vectorstore(self, path: str = "embeddings/vectorstore") -> None:
269
+ """حفظ Vector Store"""
270
+ self.vectorstore.save(path)
271
+ print(f"💾 تم حفظ Vector Store في: {path}")
272
+
273
+ def load_vectorstore(self, path: str = "embeddings/vectorstore") -> bool:
274
+ """تحميل Vector Store محفوظ"""
275
+ if self.vectorstore.load(path):
276
+ print(f"📂 تم تحميل Vector Store من: {path}")
277
+ return True
278
+ return False
279
+
280
+ def ask(self, question: str, k: int = 5) -> Dict:
281
+ """
282
+ طرح سؤال على الـ Chatbot
283
+
284
+ Args:
285
+ question: السؤال
286
+ k: عدد المستندات للاسترجاع
287
+
288
+ Returns:
289
+ dict مع الإجابة والمصادر
290
+ """
291
+ if not self.vectorstore.documents:
292
+ return {
293
+ "success": False,
294
+ "answer": "❌ النظام غير جاهز. يرجى بناء قاعدة المعرفة أولاً.",
295
+ "sources": [],
296
+ "error": "Vector store not initialized"
297
+ }
298
+
299
+ try:
300
+ # البحث عن المستندات المتشابهة
301
+ docs = self.vectorstore.similarity_search(question, k=k)
302
+
303
+ # إنشاء السياق
304
+ context = "\n\n".join([doc.page_content for doc in docs])
305
+
306
+ # إنشاء Prompt والاستدعاء
307
+ prompt = self._create_prompt(context, question)
308
+ answer = self._call_ollama(prompt)
309
+
310
+ # استخراج المصادر
311
+ sources = []
312
+ for doc in docs:
313
+ sources.append({
314
+ "content": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content,
315
+ "metadata": doc.metadata
316
+ })
317
+
318
+ return {
319
+ "success": True,
320
+ "answer": answer,
321
+ "sources": sources,
322
+ "question": question
323
+ }
324
+
325
+ except Exception as e:
326
+ return {
327
+ "success": False,
328
+ "answer": f"❌ حدث خطأ: {str(e)}",
329
+ "sources": [],
330
+ "error": str(e)
331
+ }
332
+
333
+ def similarity_search(self, query: str, k: int = 5) -> List[Document]:
334
+ """بحث التشابه"""
335
+ return self.vectorstore.similarity_search(query, k)
336
+
337
+ def similarity_search_with_score(self, query: str, k: int = 5) -> List[Tuple[Document, float]]:
338
+ """بحث التشابه مع الدرجات"""
339
+ return self.vectorstore.similarity_search_with_score(query, k)
340
+
341
+ def add_documents(self, documents: List[Document]) -> None:
342
+ """إضافة مستندات جديدة"""
343
+ self.vectorstore.add_documents(documents)
344
+ print(f"➕ تم إضافة {len(documents)} مستند جديد")
345
+
346
+ def get_relevant_context(self, question: str, k: int = 3) -> str:
347
+ """الحصول على السياق المناسب للسؤال"""
348
+ docs = self.similarity_search(question, k=k)
349
+ return "\n\n".join([doc.page_content for doc in docs])
350
+
351
+ def get_stats(self) -> Dict:
352
+ """الحصول على إحصائيات النظام"""
353
+ return {
354
+ "embedding_model": self.embedding_model_name,
355
+ "llm_model": self.llm_model,
356
+ "ollama_url": self.ollama_base_url,
357
+ "vectorstore_ready": len(self.vectorstore.documents) > 0,
358
+ "vectorstore_size": len(self.vectorstore.documents)
359
+ }
360
+
361
+
362
+ if __name__ == "__main__":
363
+ # اختبار سريع
364
+ print("=" * 50)
365
+ print("🧪 اختبار نظام RAG")
366
+ print("=" * 50)
367
+
368
+ # تهيئة النظام
369
+ rag = HRChatbotRAG(llm_model="llama3.1")
370
+
371
+ # إنشاء مستندات تجريبية
372
+ test_docs = [
373
+ Document(
374
+ page_content="السؤال: كم عدد أيام الإجازة السنوية؟\nالإجابة: يحق لكل موظف 21 يوم إجازة سنوية مدفوعة الأجر بعد إكمال سنة من العمل.",
375
+ metadata={"source": "test", "type": "qa"}
376
+ ),
377
+ Document(
378
+ page_content="السؤال: متى يتم صرف الراتب؟\nالإجابة: يتم صرف الرواتب يوم 27 من كل شهر. لو اليوم ده إجازة رسمية، بينزل المرتب في يوم العمل اللي قبله.",
379
+ metadata={"source": "test", "type": "qa"}
380
+ ),
381
+ ]
382
+
383
+ # بناء Vector Store
384
+ rag.build_vectorstore(test_docs)
385
+
386
+ # اختبار سؤال
387
+ print("\n" + "=" * 50)
388
+ result = rag.ask("كم يوم إجازة عندي؟")
389
+ print(f"✅ الإجابة: {result['answer']}")
390
+ print("=" * 50)