PDF / app.py
kawkabelaloom's picture
requirements.txt
62167ba verified
"""
🤖 نظام RAG كامل للمستندات - نسخة HuggingFace Spaces
🎯 إصدار نظيف بدون أي تبعيات لـ Google Colab
📚 يدعم العربية والإنجليزية - معالجة ملفات PDF كبيرة
"""
# ==================== 1️⃣ استيراد المكتبات ====================
import os
import sys
import numpy as np
import faiss
import nltk
from pypdf import PdfReader
from sentence_transformers import SentenceTransformer
from nltk.tokenize import word_tokenize
import pickle
import warnings
warnings.filterwarnings('ignore')
# ==================== 2️⃣ تحميل بيانات NLTK (مرة واحدة) ====================
def download_nltk_resources():
"""تحميل موارد NLTK المطلوبة"""
try:
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
print("✅ موارد NLTK جاهزة")
except Exception as e:
print(f"⚠️ ملاحظة: بعض موارد NLTK غير متوفرة: {e}")
# ==================== 3️⃣ فئات النظام الأساسية ====================
class PDFProcessor:
"""معالج PDF ذكي"""
def __init__(self, chunk_size=350, overlap=70):
self.chunk_size = chunk_size
self.overlap = overlap
def read_pdf(self, pdf_path):
"""قراءة PDF واستخراج النص"""
print(f"📖 جاري قراءة: {os.path.basename(pdf_path)}")
try:
reader = PdfReader(pdf_path)
total_pages = len(reader.pages)
pages_data = []
for i in range(total_pages):
try:
page = reader.pages[i]
text = page.extract_text()
if text and text.strip():
pages_data.append({
'page_num': i + 1,
'text': text.strip(),
'char_count': len(text)
})
# عرض التقدم
if (i + 1) % 100 == 0 or i == total_pages - 1:
print(f" 📄 تمت {i + 1}/{total_pages} صفحة")
except Exception as page_error:
print(f" ⚠️ خطأ في صفحة {i+1}: {page_error}")
continue
print(f"✅ تم قراءة {len(pages_data)} صفحة تحتوي على نص")
if pages_data:
total_chars = sum(p['char_count'] for p in pages_data)
total_words = sum(len(p['text'].split()) for p in pages_data)
print(f" 📊 إجمالي الأحرف: {total_chars:,}")
print(f" 📊 إجمالي الكلمات: {total_words:,}")
return pages_data
except Exception as e:
print(f"❌ فشل في قراءة PDF: {e}")
return []
def chunk_text(self, pages_data):
"""تقسيم النص إلى أجزاء ذكية"""
print(f"✂️ جاري تقسيم النص إلى أجزاء...")
all_chunks = []
chunk_id = 0
for page in pages_data:
text = page['text']
page_num = page['page_num']
# استخدام تقسيم بسيط للكلمات
words = text.split()
if len(words) == 0:
continue
# تقسيم النص مع التداخل
start = 0
while start < len(words):
end = start + self.chunk_size
chunk_words = words[start:end]
if chunk_words:
chunk_text = ' '.join(chunk_words)
all_chunks.append({
'chunk_id': chunk_id,
'text': chunk_text,
'page': page_num,
'word_count': len(chunk_words),
'start_word': start,
'end_word': min(end, len(words))
})
chunk_id += 1
start += self.chunk_size - self.overlap
print(f"✅ تم إنشاء {len(all_chunks)} جزء نصي")
if all_chunks:
avg_words = sum(c['word_count'] for c in all_chunks) // len(all_chunks)
print(f" 📊 متوسط الكلمات لكل جزء: {avg_words}")
return all_chunks
class VectorStore:
"""مخزن المتجهات باستخدام FAISS"""
def __init__(self, model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"):
self.model_name = model_name
self.model = None
self.index = None
self.chunks = None
self.embeddings = None
def load_model(self):
"""تحميل نموذج Embeddings"""
print(f"🚀 جاري تحميل نموذج: {self.model_name}")
try:
self.model = SentenceTransformer(self.model_name)
print(f"✅ تم تحميل النموذج بنجاح")
print(f" 📏 أبعاد المتجهات: {self.model.get_sentence_embedding_dimension()}")
return True
except Exception as e:
print(f"❌ خطأ في تحميل النموذج: {e}")
return False
def create_embeddings(self, chunks):
"""إنشاء Embeddings للنصوص"""
print(f"🧠 جاري إنشاء Embeddings لـ {len(chunks)} جزء...")
self.chunks = chunks
chunk_texts = [chunk['text'] for chunk in chunks]
try:
self.embeddings = self.model.encode(
chunk_texts,
show_progress_bar=True,
normalize_embeddings=True,
batch_size=32,
convert_to_numpy=True
)
print(f"✅ تم إنشاء {len(self.embeddings)} متجه embedding")
return True
except Exception as e:
print(f"❌ خطأ في إنشاء Embeddings: {e}")
return False
def build_index(self):
"""بناء فهرس FAISS"""
if self.embeddings is None:
print("❌ لا توجد embeddings لبناء الفهرس")
return False
print("🔧 جاري بناء Vector Store...")
try:
dimension = self.embeddings.shape[1]
self.index = faiss.IndexFlatIP(dimension)
# تطبيع وإضافة المتجهات
faiss.normalize_L2(self.embeddings)
self.index.add(self.embeddings)
print(f"✅ تم بناء Vector Store: {self.index.ntotal} متجه")
return True
except Exception as e:
print(f"❌ خطأ في بناء الفهرس: {e}")
return False
def save_index(self, path="vector_store"):
"""حفظ الفهرس للاستخدام المستقبلي"""
try:
# حفظ الفهرس
faiss.write_index(self.index, f"{path}.faiss")
# حفظ البيانات النصية
with open(f"{path}_chunks.pkl", "wb") as f:
pickle.dump(self.chunks, f)
print(f"💾 تم حفظ الفهرس والبيانات في: {path}")
return True
except Exception as e:
print(f"❌ خطأ في حفظ الفهرس: {e}")
return False
def load_index(self, path="vector_store"):
"""تحميل الفهرس المحفوظ"""
try:
# تحميل الفهرس
self.index = faiss.read_index(f"{path}.faiss")
# تحميل البيانات النصية
with open(f"{path}_chunks.pkl", "rb") as f:
self.chunks = pickle.load(f)
print(f"📂 تم تحميل الفهرس: {self.index.ntotal} متجه")
print(f"📂 تم تحميل البيانات: {len(self.chunks)} جزء")
return True
except Exception as e:
print(f"❌ خطأ في تحميل الفهرس: {e}")
return False
def search(self, query, top_k=5, similarity_threshold=0.25):
"""بحث دلالي في المستندات"""
if self.index is None or self.model is None or self.chunks is None:
print("❌ النظام غير مهيء للبحث")
return []
# إنشاء embedding للاستعلام
query_embedding = self.model.encode([query], normalize_embeddings=True)
# البحث عن عدد أكبر ثم تصفية
search_k = top_k * 3
scores, indices = self.index.search(query_embedding, search_k)
# تجميع النتائج المؤهلة
results = []
for i, (score, idx) in enumerate(zip(scores[0], indices[0])):
# التحقق من أن الفهرس صالح والتشابه مقبول
if 0 <= idx < len(self.chunks) and score >= similarity_threshold:
chunk = self.chunks[idx]
results.append({
'rank': len(results) + 1,
'score': float(score),
'similarity_percent': f"{score * 100:.1f}%",
'similarity_raw': score,
'text': chunk['text'],
'page': chunk['page'],
'word_count': chunk['word_count'],
'preview': chunk['text'][:150] + "..." if len(chunk['text']) > 150 else chunk['text']
})
# التوقف عند الوصول إلى العدد المطلوب
if len(results) >= top_k:
break
return results
class RAGSystem:
"""النظام الرئيسي RAG"""
def __init__(self):
self.processor = PDFProcessor()
self.vector_store = VectorStore()
self.is_ready = False
def initialize(self):
"""تهيئة النظام"""
print("=" * 60)
print("🤖 نظام RAG للمستندات الذكي")
print("=" * 60)
# تحميل موارد NLTK
download_nltk_resources()
# تحميل نموذج Embeddings
if not self.vector_store.load_model():
return False
self.is_ready = True
return True
def process_pdf(self, pdf_path):
"""معالجة ملف PDF جديد"""
if not self.is_ready:
print("❌ النظام غير مهيء")
return False
# قراءة PDF
pages_data = self.processor.read_pdf(pdf_path)
if not pages_data:
return False
# تقسيم النص
chunks = self.processor.chunk_text(pages_data)
if not chunks:
return False
# إنشاء embeddings وفهرس
if not self.vector_store.create_embeddings(chunks):
return False
if not self.vector_store.build_index():
return False
print("✨ تم معالجة المستند بنجاح!")
return True
def ask_question(self, question, top_k=3, similarity_threshold=0.25):
"""طرح سؤال على النظام"""
if not self.is_ready or self.vector_store.index is None:
return {
'success': False,
'error': 'النظام غير مهيء. يرجى معالجة مستند أولاً.',
'results': []
}
print(f"\n🔍 البحث عن: '{question}'")
# البحث في المستند
results = self.vector_store.search(question, top_k, similarity_threshold)
if not results:
return {
'success': False,
'error': 'لم أجد نتائج ذات صلة في المستند.',
'results': []
}
# تقييم جودة النتائج
evaluation = self._evaluate_results(results)
return {
'success': True,
'question': question,
'results': results,
'evaluation': evaluation,
'total_results': len(results),
'best_similarity': results[0]['similarity_percent'] if results else "0%"
}
def _evaluate_results(self, results):
"""تقييم جودة نتائج البحث"""
if not results:
return "❌ لا توجد نتائج للتقييم"
# حساب متوسط التشابه
similarities = [r['similarity_raw'] for r in results]
avg_similarity = sum(similarities) / len(similarities) * 100
# تحديد الجودة
if avg_similarity >= 50:
quality = "ممتازة 🏆"
emoji = "✅"
elif avg_similarity >= 40:
quality = "جيدة 👍"
emoji = "✓"
elif avg_similarity >= 30:
quality = "متوسطة ⚠️"
emoji = "~"
else:
quality = "ضعيفة ❌"
emoji = "✗"
# حساب تغطية الصفحات
unique_pages = len(set(r['page'] for r in results))
evaluation = f"""
📊 **تقرير التقييم:**
{emoji} **الجودة:** {quality}
📈 **متوسط التشابه:** {avg_similarity:.1f}%
🔢 **أفضل نتيجة:** {results[0]['similarity_percent']}
📖 **صفحات مختلفة:** {unique_pages}
📝 **إجمالي النتائج:** {len(results)}
"""
return evaluation
def save_state(self, path="rag_system_state"):
"""حفظ حالة النظام"""
return self.vector_store.save_index(path)
def load_state(self, path="rag_system_state"):
"""تحميل حالة النظام"""
if not self.vector_store.load_model():
return False
if self.vector_store.load_index(path):
self.is_ready = True
return True
return False
# ==================== 4️⃣ واجهة Streamlit لـ HuggingFace ====================
def create_streamlit_app():
"""إنشاء واجهة ويب باستخدام Streamlit"""
try:
import streamlit as st
from streamlit.runtime.uploaded_file_manager import UploadedFile
# إعداد صفحة Streamlit
st.set_page_config(
page_title="نظام RAG الذكي للمستندات",
page_icon="🤖",
layout="wide"
)
# CSS مخصص
st.markdown("""
<style>
.main-header {
text-align: center;
padding: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin-bottom: 2rem;
}
.result-card {
background: #f8f9fa;
border-radius: 10px;
padding: 1rem;
margin: 1rem 0;
border-left: 5px solid #667eea;
}
.similarity-high {
color: #28a745;
font-weight: bold;
}
.similarity-medium {
color: #ffc107;
font-weight: bold;
}
.similarity-low {
color: #dc3545;
font-weight: bold;
}
</style>
""", unsafe_allow_html=True)
# العنوان الرئيسي
st.markdown("""
<div class="main-header">
<h1>🤖 نظام RAG الذكي للمستندات</h1>
<p>بحث ذكي في ملفات PDF - يدعم العربية والإنجليزية</p>
</div>
""", unsafe_allow_html=True)
# تهيئة النظام في حالة الجلسة
if 'rag_system' not in st.session_state:
with st.spinner("🚀 جاري تهيئة النظام..."):
st.session_state.rag_system = RAGSystem()
if st.session_state.rag_system.initialize():
st.success("✅ تم تهيئة النظام بنجاح!")
else:
st.error("❌ فشل في تهيئة النظام")
return
rag_system = st.session_state.rag_system
# الشريط الجانبي
with st.sidebar:
st.header("⚙️ الإعدادات")
# تحميل ملف PDF
st.subheader("📁 رفع ملف PDF")
uploaded_file = st.file_uploader(
"اختر ملف PDF",
type=["pdf"],
help="يمكنك رفع أي ملف PDF للبحث فيه"
)
if uploaded_file is not None:
# حفظ الملف المؤقت
temp_path = f"temp_{uploaded_file.name}"
with open(temp_path, "wb") as f:
f.write(uploaded_file.getbuffer())
# معالجة الملف
if st.button("🚀 معالجة المستند", type="primary"):
with st.spinner("جاري معالجة المستند..."):
if rag_system.process_pdf(temp_path):
st.success(f"✅ تم معالجة: {uploaded_file.name}")
st.session_state.processed_file = uploaded_file.name
else:
st.error("❌ فشل في معالجة الملف")
# إعدادات البحث
st.subheader("🔍 إعدادات البحث")
top_k = st.slider("عدد النتائج", 1, 10, 3)
similarity_threshold = st.slider("عتبة التشابه", 0.0, 1.0, 0.25, 0.05)
# معلومات النظام
st.subheader("📊 معلومات النظام")
if rag_system.is_ready and rag_system.vector_store.chunks:
st.info(f"📄 الأجزاء النصية: {len(rag_system.vector_store.chunks)}")
st.info(f"🧮 المتجهات: {rag_system.vector_store.index.ntotal if rag_system.vector_store.index else 0}")
# المنطقة الرئيسية
col1, col2 = st.columns([2, 1])
with col1:
st.header("💬 اسأل عن المستند")
# حقل إدخال السؤال
question = st.text_area(
"اكتب سؤالك هنا",
placeholder="مثال: ما هي حالة التدفق؟ أو What is flow state?",
height=100
)
# أزرار الأسئلة السريعة
st.subheader("💡 أسئلة سريعة")
quick_questions = [
"ما هي حالة التدفق؟",
"What is flow state?",
"ما هي عناصر التجربة المثلى؟",
"كيف يحقق الإنسان السعادة في العمل؟"
]
cols = st.columns(4)
for idx, q in enumerate(quick_questions):
if cols[idx].button(q, use_container_width=True):
question = q
with col2:
st.header("🎯 نصائح البحث")
st.info("""
**للحصول على أفضل النتائج:**
1. استخدم مصطلحات محددة
2. جرب اللغتين (عربي/إنجليزي)
3. اطرح أسئلة واضحة
4. استخدم مصطلحات الكتاب
**مثال:**
✅ "ما هي خصائص flow state؟"
❌ "شرح لي"
""")
# زر البحث
if st.button("🔍 ابحث في المستند", type="primary", use_container_width=True):
if not question:
st.warning("⚠️ يرجى إدخال سؤال")
elif not (rag_system.is_ready and rag_system.vector_store.chunks):
st.error("❌ يرجى معالجة مستند أولاً")
else:
with st.spinner("جاري البحث..."):
result = rag_system.ask_question(
question,
top_k=top_k,
similarity_threshold=similarity_threshold
)
if result['success']:
# عرض التقييم
with st.expander("📊 تقرير التقييم", expanded=True):
st.markdown(result['evaluation'])
# عرض النتائج
st.subheader(f"📄 النتائج ({len(result['results'])})")
for r in result['results']:
# تحديد لون التشابه
similarity = r['similarity_raw']
if similarity >= 0.5:
sim_class = "similarity-high"
elif similarity >= 0.3:
sim_class = "similarity-medium"
else:
sim_class = "similarity-low"
# عرض البطاقة
with st.container():
st.markdown(f"""
<div class="result-card">
<h4>🏆 النتيجة #{r['rank']}</h4>
<p><span class="{sim_class}">التشابه: {r['similarity_percent']}</span> | 📖 الصفحة: {r['page']} | 🔢 الكلمات: {r['word_count']}</p>
<hr>
<p>{r['text']}</p>
</div>
""", unsafe_allow_html=True)
else:
st.error(result['error'])
# قسم الأمثلة التوضيحية
with st.expander("📖 أمثلة توضيحية", expanded=False):
st.markdown("""
**مستند كتاب Flow:**
- "ما هي حالة التدفق flow state؟"
- "What are the characteristics of optimal experience?"
- "كيف يرتبط التحدي بالمهارة في نظرية التدفق؟"
**مستندات أخرى:**
- "ما هو الموضوع الرئيسي؟"
- "ما هي النقاط المهمة؟"
- "هل هناك أمثلة عملية؟"
""")
# تذييل الصفحة
st.markdown("---")
st.markdown("""
<div style="text-align: center; color: #666;">
<p>🤖 نظام RAG للمستندات | إصدار HuggingFace</p>
<p>تقنية: FAISS + Sentence Transformers + Streamlit</p>
</div>
""", unsafe_allow_html=True)
except ImportError:
print("⚠️ Streamlit غير مثبت. لتشغيل الواجهة:")
print(" pip install streamlit")
print(" streamlit run app.py")
# ==================== 5️⃣ التشغيل الرئيسي ====================
def main_cli():
"""واجهة سطر الأوامر"""
print("=" * 60)
print("🤖 نظام RAG للمستندات - واجهة سطر الأوامر")
print("=" * 60)
# إنشاء النظام
rag_system = RAGSystem()
# تهيئة النظام
if not rag_system.initialize():
print("❌ فشل في تهيئة النظام")
return
# قائمة الأوامر
commands = """
🎮 الأوامر المتاحة:
1. معالجة - معالجة ملف PDF جديد
2. بحث - البحث في المستند
3. حفظ - حفظ حالة النظام
4. تحميل - تحميل حالة محفوظة
5. خروج - إنهاء البرنامج
6. ويب - تشغيل واجهة الويب (يتطلب Streamlit)
"""
while True:
print("\n" + commands)
command = input("\n📝 أدخل الأمر: ").strip().lower()
if command in ['خروج', 'exit', '5']:
print("👋 مع السلامة!")
break
elif command in ['معالجة', '1']:
pdf_path = input("📁 أدخل مسار ملف PDF: ").strip()
if os.path.exists(pdf_path):
rag_system.process_pdf(pdf_path)
else:
print(f"❌ الملف غير موجود: {pdf_path}")
elif command in ['بحث', '2']:
if not rag_system.is_ready or rag_system.vector_store.index is None:
print("❌ يرجى معالجة مستند أولاً")
continue
question = input("🧠 أدخل سؤالك: ").strip()
if question:
result = rag_system.ask_question(question)
if result['success']:
print(result['evaluation'])
for r in result['results']:
print(f"\n🏆 النتيجة #{r['rank']}")
print(f" 📈 التشابه: {r['similarity_percent']}")
print(f" 📖 الصفحة: {r['page']}")
print(f" 📝 المحتوى: {r['text'][:200]}...")
else:
print(result['error'])
elif command in ['حفظ', '3']:
path = input("💾 أدخل اسم الملف للحفظ (دون امتداد): ").strip()
if path:
rag_system.save_state(path)
elif command in ['تحميل', '4']:
path = input("📂 أدخل اسم الملف للتحميل (دون امتداد): ").strip()
if path:
rag_system.load_state(path)
elif command in ['ويب', '6']:
print("🌐 جاري تشغيل واجهة الويب...")
print(" تأكد من تثبيت Streamlit أولاً: pip install streamlit")
print(" ثم شغل: streamlit run app.py")
break
else:
print("⚠️ أمر غير معروف")
# ==================== 6️⃣ نقطة الدخول الرئيسية ====================
if __name__ == "__main__":
# اختيار وضع التشغيل
print("=" * 60)
print("🎮 اختر وضع التشغيل:")
print("1. واجهة سطر الأوامر (CLI)")
print("2. واجهة الويب (يتطلب Streamlit)")
print("=" * 60)
try:
choice = input("📝 أدخل رقم الخيار: ").strip()
if choice == "1":
main_cli()
elif choice == "2":
create_streamlit_app()
else:
print("⚠️ خيار غير صحيح، تشغيل CLI...")
main_cli()
except KeyboardInterrupt:
print("\n\n👋 تم إيقاف البرنامج")
except Exception as e:
print(f"\n❌ حدث خطأ: {e}")