Loading
Loading
هذا المشروع يجمع كل مهارات دورة Generative AI في تطبيق واقعي: Chatbot يتحدث مع PDF بأي لغة.
PDF Chatbot
├── 📄 رفع PDF وتحليله تلقائياً
├── ✂️ Chunking ذكي بالفقرات
├── 🧠 Embeddings + ChromaDB
├── 💬 محادثة طبيعية مع RAG
└── 🔄 ذاكرة المحادثة (Multi-turn)
pip install anthropic chromadb sentence-transformers pypdf2 rich
| الدرس | ما تعلمته | |-------|---------| | 1. كيف تعمل LLMs | Transformer، Tokenization، Hallucination | | 2. Embeddings | Cosine Similarity، Chunking، Semantic Search | | 3. Vector Databases | ChromaDB، HNSW، Metadata Filtering | | 4. RAG من الصفر | Indexing Pipeline، Retrieval، RAG Prompt | | 5. المشروع | PDF Chatbot كامل مع Multi-turn |
بعد إنهاء المشروع، يمكنك:
ابنِ الـ Chatbot كاملاً ثم جرّبه على:
لاحظ كيف تتغير جودة الإجابات مع تغيير حجم الـ chunk وعدد الـ results.
# PDF Chatbot — نظام RAG كامل مع ذاكرة محادثة
# pip install anthropic chromadb sentence-transformers pypdf2 rich
import sys
import anthropic
import chromadb
from sentence_transformers import SentenceTransformer
from pathlib import Path
try:
import PyPDF2
HAS_PDF = True
except ImportError:
HAS_PDF = False
print("⚠️ pip install pypdf2 لدعم PDF")
try:
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
console = Console()
USE_RICH = True
except ImportError:
USE_RICH = False
# ─────────────────────────────────────────
# أدوات
# ─────────────────────────────────────────
def extract_pdf_text(pdf_path: str) -> str:
"""استخراج النص من PDF"""
if not HAS_PDF:
return "خطأ: مكتبة PyPDF2 غير مثبتة"
text_parts = []
with open(pdf_path, "rb") as f:
reader = PyPDF2.PdfReader(f)
for page_num, page in enumerate(reader.pages, 1):
text = page.extract_text()
if text:
text_parts.append(f"[صفحة {page_num}]\n{text}")
return "\n\n".join(text_parts)
def smart_chunk(text: str, min_size: int = 100, max_size: int = 500) -> list[str]:
"""تقسيم ذكي بالفقرات مع حد أدنى وأقصى"""
# تقسيم بالفقرات أولاً
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks, current_chunk = [], ""
for para in paragraphs:
if len(current_chunk) + len(para) <= max_size:
current_chunk += (("\n\n" + para) if current_chunk else para)
else:
if len(current_chunk) >= min_size:
chunks.append(current_chunk)
current_chunk = para
if current_chunk and len(current_chunk) >= min_size:
chunks.append(current_chunk)
return chunks
# ─────────────────────────────────────────
# PDF Chatbot Class
# ─────────────────────────────────────────
class PDFChatbot:
def __init__(self):
self.claude = anthropic.Anthropic()
self.embed_model = SentenceTransformer("all-MiniLM-L6-v2")
self.chroma = chromadb.Client()
self.collection = None
self.pdf_name = ""
self.chat_history: list[dict] = []
self.chunk_count = 0
def load_pdf(self, pdf_path: str) -> bool:
"""تحميل PDF وبناء قاعدة المعرفة"""
path = Path(pdf_path)
if not path.exists():
print(f"❌ الملف غير موجود: {pdf_path}")
return False
print(f"\n📄 جاري قراءة: {path.name}")
text = extract_pdf_text(pdf_path)
if not text or len(text) < 100:
print("❌ تعذّر استخراج النص من PDF")
return False
print(f"✅ استُخرج {len(text)} حرف")
# Chunking
chunks = smart_chunk(text)
print(f"✂️ تقسيم إلى {len(chunks)} chunk")
# إنشاء collection جديدة
try:
self.chroma.delete_collection("pdf_chat")
except Exception:
pass
self.collection = self.chroma.create_collection("pdf_chat")
# Embedding وإضافة
print("🧠 توليد Embeddings...")
embeddings = self.embed_model.encode(chunks).tolist()
self.collection.add(
ids=[f"c{i}" for i in range(len(chunks))],
documents=chunks,
embeddings=embeddings
)
self.pdf_name = path.name
self.chunk_count = len(chunks)
self.chat_history = []
print(f"✅ جاهز للمحادثة! ({len(chunks)} chunk مفهرس)")
return True
def ask(self, question: str, top_k: int = 4) -> str:
"""اطرح سؤالاً على الـ PDF"""
if not self.collection:
return "❌ لم يتم تحميل أي PDF بعد"
# استرجاع الـ chunks ذات الصلة
q_emb = self.embed_model.encode([question]).tolist()
results = self.collection.query(query_embeddings=q_emb, n_results=top_k)
context = "\n\n---\n\n".join(results["documents"][0])
# بناء سياق المحادثة (Multi-turn)
history_text = ""
if self.chat_history:
last_3 = self.chat_history[-3:] # آخر 3 أزواج
for turn in last_3:
history_text += f"المستخدم: {turn['q']}\nالمساعد: {turn['a'][:200]}...\n\n"
# الـ Prompt
prompt = f"""أنت مساعد ذكي متخصص في تحليل المستندات.
تحدث مع المستخدم باللغة التي يستخدمها (عربي/إنجليزي).
أجب بناءً على السياق المقدم فقط. إذا كانت المعلومات غير موجودة في السياق، قل ذلك.
{f"سياق المحادثة السابقة:\n{history_text}" if history_text else ""}
محتوى ذو صلة من المستند ({self.pdf_name}):
{context}
سؤال المستخدم: {question}"""
msg = self.claude.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=600,
messages=[{"role": "user", "content": prompt}]
)
answer = msg.content[0].text
# حفظ في السجل
self.chat_history.append({"q": question, "a": answer})
return answer
# ─────────────────────────────────────────
# واجهة المستخدم
# ─────────────────────────────────────────
def main():
bot = PDFChatbot()
print("=" * 60)
print("📚 PDF Chatbot — تحدّث مع أي PDF")
print("=" * 60)
print("الأوامر: /load <مسار_PDF> | /clear | /exit")
# تحميل PDF تجريبي إذا لم يُحدَّد
sample_path = sys.argv[1] if len(sys.argv) > 1 else None
if sample_path:
bot.load_pdf(sample_path)
while True:
try:
user_input = input("\nأنت: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n👋 إلى اللقاء!")
break
if not user_input:
continue
if user_input.startswith("/load "):
pdf_path = user_input[6:].strip()
bot.load_pdf(pdf_path)
elif user_input == "/clear":
bot.chat_history = []
print("✅ تم مسح سجل المحادثة")
elif user_input in ["/exit", "/خروج"]:
print("👋 إلى اللقاء!")
break
elif not bot.collection:
print("⚠️ استخدم: /load <مسار_PDF> لتحميل مستند أولاً")
else:
answer = bot.ask(user_input)
print(f"\nالمساعد: {answer}")
if __name__ == "__main__":
main()