""" نظام RAG للـ HR Chatbot باستخدام Ollama و Llama 3.1 HR Chatbot RAG System with Ollama & Llama 3.1 هذه النسخة تستخدم: - sentence-transformers للـ Embeddings - FAISS للـ Vector Store - Ollama API مباشرة للـ LLM """ import os import json import requests import numpy as np from typing import List, Dict, Optional, Tuple class Document: """مستند بسيط للـ RAG""" def __init__(self, page_content: str, metadata: dict = None): self.page_content = page_content self.metadata = metadata or {} class SimpleVectorStore: """Vector Store بسيط باستخدام FAISS""" def __init__(self, embedding_model): self.embedding_model = embedding_model self.documents: List[Document] = [] self.embeddings: Optional[np.ndarray] = None self.index = None def add_documents(self, documents: List[Document]): """إضافة مستندات""" # استخراج النصوص texts = [doc.page_content for doc in documents] # إنشاء embeddings new_embeddings = self.embedding_model.encode(texts, convert_to_numpy=True) if self.embeddings is None: self.embeddings = new_embeddings self.documents = documents else: self.embeddings = np.vstack([self.embeddings, new_embeddings]) self.documents.extend(documents) # بناء FAISS index self._build_index() def _build_index(self): """بناء FAISS index""" import faiss if self.embeddings is None: return # تطبيع الـ embeddings embeddings_copy = self.embeddings.copy().astype('float32') faiss.normalize_L2(embeddings_copy) # إنشاء index dimension = embeddings_copy.shape[1] self.index = faiss.IndexFlatIP(dimension) # Inner Product (Cosine Similarity) self.index.add(embeddings_copy) def similarity_search(self, query: str, k: int = 5) -> List[Document]: """بحث التشابه""" if self.index is None: return [] import faiss # تحويل السؤال إلى embedding query_embedding = self.embedding_model.encode([query], convert_to_numpy=True).astype('float32') faiss.normalize_L2(query_embedding) # البحث scores, indices = self.index.search(query_embedding, k) results = [] for idx in indices[0]: if 0 <= idx < len(self.documents): results.append(self.documents[idx]) return results def similarity_search_with_score(self, query: str, k: int = 5) -> List[Tuple[Document, float]]: """بحث التشابه مع الدرجات""" if self.index is None: return [] import faiss # تحويل السؤال إلى embedding query_embedding = self.embedding_model.encode([query], convert_to_numpy=True).astype('float32') faiss.normalize_L2(query_embedding) # البحث scores, indices = self.index.search(query_embedding, k) results = [] for score, idx in zip(scores[0], indices[0]): if 0 <= idx < len(self.documents): results.append((self.documents[idx], float(score))) return results def save(self, path: str): """حفظ Vector Store""" import faiss os.makedirs(path, exist_ok=True) # حفظ FAISS index if self.index is not None: faiss.write_index(self.index, os.path.join(path, "index.faiss")) # حفظ المستندات docs_data = [{"content": doc.page_content, "metadata": doc.metadata} for doc in self.documents] with open(os.path.join(path, "documents.json"), "w", encoding="utf-8") as f: json.dump(docs_data, f, ensure_ascii=False, indent=2) # حفظ embeddings if self.embeddings is not None: np.save(os.path.join(path, "embeddings.npy"), self.embeddings) def load(self, path: str) -> bool: """تحميل Vector Store""" import faiss try: # تحميل FAISS index index_path = os.path.join(path, "index.faiss") if os.path.exists(index_path): self.index = faiss.read_index(index_path) # تحميل المستندات docs_path = os.path.join(path, "documents.json") if os.path.exists(docs_path): with open(docs_path, "r", encoding="utf-8") as f: docs_data = json.load(f) self.documents = [Document(d["content"], d["metadata"]) for d in docs_data] # تحميل embeddings emb_path = os.path.join(path, "embeddings.npy") if os.path.exists(emb_path): self.embeddings = np.load(emb_path) return True except Exception as e: print(f"❌ خطأ في تحميل Vector Store: {e}") return False class HRChatbotRAG: """نظام RAG للـ HR Chatbot باستخدام Ollama""" def __init__( self, embedding_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", llm_model: str = "llama3.1", ollama_base_url: str = "http://localhost:11434", temperature: float = 0.3, streaming: bool = False ): """ تهيئة نظام RAG Args: embedding_model: نموذج الـ Embeddings llm_model: نموذج Ollama (llama3.1) ollama_base_url: رابط Ollama temperature: درجة الإبداعية streaming: تفعيل البث المباشر """ self.embedding_model_name = embedding_model self.llm_model = llm_model self.ollama_base_url = ollama_base_url self.temperature = temperature self.streaming = streaming print(f"🚀 جاري تهيئة نظام RAG...") print(f" 📦 Embedding Model: {embedding_model}") print(f" 🤖 LLM Model: {llm_model} (Ollama)") # تهيئة Embeddings print(" ⏳ تحميل نموذج Embeddings...") from sentence_transformers import SentenceTransformer self.embedding_model = SentenceTransformer(embedding_model) # تهيئة Vector Store self.vectorstore = SimpleVectorStore(self.embedding_model) # التحقق من الاتصال بـ Ollama print(" ⏳ الاتصال بـ Ollama...") if self._test_ollama_connection(): print(f" ✅ تم الاتصال بـ Ollama بنجاح!") else: print(f" ⚠️ تحذير: تأكد من تشغيل Ollama!") print("✅ تم تهيئة النظام بنجاح!") def _test_ollama_connection(self) -> bool: """اختبار الاتصال بـ Ollama""" try: response = requests.get(f"{self.ollama_base_url}/api/tags", timeout=5) return response.status_code == 200 except: return False def _call_ollama(self, prompt: str) -> str: """استدعاء Ollama API""" try: response = requests.post( f"{self.ollama_base_url}/api/generate", json={ "model": self.llm_model, "prompt": prompt, "stream": False, "options": { "temperature": self.temperature, "num_ctx": 4096, "num_predict": 512 } }, timeout=120 ) if response.status_code == 200: return response.json().get("response", "") else: return f"خطأ في Ollama: {response.status_code}" except Exception as e: return f"خطأ في الاتصال بـ Ollama: {str(e)}" def _create_prompt(self, context: str, question: str) -> str: """إنشاء Prompt للـ HR Chatbot""" return f"""أنت مساعد ذكي لنظام الموارد البشرية (HR Genie). مهمتك هي الإجابة على أسئلة الموظفين بشكل دقيق ومهني باللغة العربية. استخدم المعلومات التالية للإجابة على السؤال. إذا لم تجد الإجابة في المعلومات المتاحة، قل ذلك بوضوح واقترح التواصل مع قسم الموارد البشرية. المعلومات المتاحة: {context} السؤال: {question} التعليمات: - أجب باللغة العربية بشكل واضح ومختصر - إذا كان السؤال عن إجراء معين، اشرح الخطوات بوضوح - كن ودوداً ومهنياً في الرد - إذا لم تكن متأكداً من الإجابة، اقترح التواصل مع قسم HR الإجابة:""" def build_vectorstore(self, documents: List[Document]) -> None: """ بناء Vector Store من المستندات Args: documents: قائمة المستندات """ print(f"🔨 جاري بناء Vector Store من {len(documents)} مستند...") self.vectorstore.add_documents(documents) print(f"✅ تم بناء Vector Store بنجاح!") def save_vectorstore(self, path: str = "embeddings/vectorstore") -> None: """حفظ Vector Store""" self.vectorstore.save(path) print(f"💾 تم حفظ Vector Store في: {path}") def load_vectorstore(self, path: str = "embeddings/vectorstore") -> bool: """تحميل Vector Store محفوظ""" if self.vectorstore.load(path): print(f"📂 تم تحميل Vector Store من: {path}") return True return False def ask(self, question: str, k: int = 5) -> Dict: """ طرح سؤال على الـ Chatbot Args: question: السؤال k: عدد المستندات للاسترجاع Returns: dict مع الإجابة والمصادر """ if not self.vectorstore.documents: return { "success": False, "answer": "❌ النظام غير جاهز. يرجى بناء قاعدة المعرفة أولاً.", "sources": [], "error": "Vector store not initialized" } try: # البحث عن المستندات المتشابهة docs = self.vectorstore.similarity_search(question, k=k) # إنشاء السياق context = "\n\n".join([doc.page_content for doc in docs]) # إنشاء Prompt والاستدعاء prompt = self._create_prompt(context, question) answer = self._call_ollama(prompt) # استخراج المصادر sources = [] for doc in docs: sources.append({ "content": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content, "metadata": doc.metadata }) return { "success": True, "answer": answer, "sources": sources, "question": question } except Exception as e: return { "success": False, "answer": f"❌ حدث خطأ: {str(e)}", "sources": [], "error": str(e) } def similarity_search(self, query: str, k: int = 5) -> List[Document]: """بحث التشابه""" return self.vectorstore.similarity_search(query, k) def similarity_search_with_score(self, query: str, k: int = 5) -> List[Tuple[Document, float]]: """بحث التشابه مع الدرجات""" return self.vectorstore.similarity_search_with_score(query, k) def add_documents(self, documents: List[Document]) -> None: """إضافة مستندات جديدة""" self.vectorstore.add_documents(documents) print(f"➕ تم إضافة {len(documents)} مستند جديد") def get_relevant_context(self, question: str, k: int = 3) -> str: """الحصول على السياق المناسب للسؤال""" docs = self.similarity_search(question, k=k) return "\n\n".join([doc.page_content for doc in docs]) def get_stats(self) -> Dict: """الحصول على إحصائيات النظام""" return { "embedding_model": self.embedding_model_name, "llm_model": self.llm_model, "ollama_url": self.ollama_base_url, "vectorstore_ready": len(self.vectorstore.documents) > 0, "vectorstore_size": len(self.vectorstore.documents) } if __name__ == "__main__": # اختبار سريع print("=" * 50) print("🧪 اختبار نظام RAG") print("=" * 50) # تهيئة النظام rag = HRChatbotRAG(llm_model="llama3.1") # إنشاء مستندات تجريبية test_docs = [ Document( page_content="السؤال: كم عدد أيام الإجازة السنوية؟\nالإجابة: يحق لكل موظف 21 يوم إجازة سنوية مدفوعة الأجر بعد إكمال سنة من العمل.", metadata={"source": "test", "type": "qa"} ), Document( page_content="السؤال: متى يتم صرف الراتب؟\nالإجابة: يتم صرف الرواتب يوم 27 من كل شهر. لو اليوم ده إجازة رسمية، بينزل المرتب في يوم العمل اللي قبله.", metadata={"source": "test", "type": "qa"} ), ] # بناء Vector Store rag.build_vectorstore(test_docs) # اختبار سؤال print("\n" + "=" * 50) result = rag.ask("كم يوم إجازة عندي؟") print(f"✅ الإجابة: {result['answer']}") print("=" * 50)