| import gradio as gr |
| import json |
| import os |
| from pathlib import Path |
| from dotenv import load_dotenv |
| from typing import List, Dict |
|
|
| from langchain_openai import ChatOpenAI, OpenAIEmbeddings |
| from langchain_chroma import Chroma |
| from langchain_core.messages import SystemMessage, HumanMessage |
| from langchain_core.documents import Document |
|
|
| |
| DATA_PATH = Path("data/allProjects.fixed.jsonl") |
| CHROMA_DIR = Path("./chroma_db_structured") |
| MODEL_NAME = "gpt-4o-mini" |
| EMBEDDING_MODEL = "text-embedding-3-small" |
|
|
| |
| load_dotenv() |
| if not os.getenv("OPENAI_API_KEY"): |
| raise EnvironmentError("OPENAI_API_KEY not found.") |
|
|
| |
| def load_projects(path: Path) -> List[Dict]: |
| projects = [] |
| with path.open("r", encoding="utf-8") as f: |
| for line in f: |
| data = json.loads(line) |
| projects.append({ |
| "id": data.get("id"), |
| "title": data.get("title", ""), |
| "content": data.get("content", ""), |
| "image": data.get("image", ""), |
| "url": data.get("url", "") |
| }) |
| return projects |
|
|
| PROJECTS = load_projects(DATA_PATH) |
| PROJECT_COUNT = len(PROJECTS) |
| PROJECTS_BY_ID = {p["id"]: p for p in PROJECTS if p.get("id")} |
| ALL_TITLES = "\n".join([f"{p['id']}: {p['title']}" for p in PROJECTS]) |
|
|
| |
| embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL) |
|
|
| def build_vectorstore(): |
| """بناء أو تحميل ChromaDB مع التحقق من وجود البيانات""" |
| vectorstore = Chroma( |
| embedding_function=embeddings, |
| persist_directory=str(CHROMA_DIR) |
| ) |
| |
| |
| existing_count = vectorstore._collection.count() |
| print(f"[ChromaDB] عدد الوثائق الموجودة: {existing_count}") |
| |
| if existing_count == 0: |
| print("[ChromaDB] قاعدة البيانات فارغة، جاري البناء...") |
| docs = [] |
| for p in PROJECTS: |
| |
| combined_text = f"عنوان المشروع: {p['title']}\n\nتفاصيل المشروع:\n{p['content']}" |
| docs.append(Document( |
| page_content=combined_text, |
| metadata={ |
| "id": p["id"], |
| "title": p["title"], |
| "url": p.get("url", ""), |
| "image": p.get("image", "") |
| } |
| )) |
| |
| |
| BATCH_SIZE = 50 |
| for i in range(0, len(docs), BATCH_SIZE): |
| batch = docs[i:i+BATCH_SIZE] |
| vectorstore.add_documents(batch) |
| print(f"[ChromaDB] تمت إضافة {min(i+BATCH_SIZE, len(docs))}/{len(docs)}") |
| |
| print(f"[ChromaDB] ✅ تم البناء بنجاح. إجمالي: {vectorstore._collection.count()}") |
| |
| return vectorstore |
|
|
| vectorstore = build_vectorstore() |
| retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) |
|
|
| |
| llm = ChatOpenAI(model=MODEL_NAME, temperature=0.5) |
|
|
| |
| def is_project_question(message: str) -> bool: |
| keywords = ["مشروع", "مشاريع", "العتبة", "العباسية", "كربلاء"] |
| return any(k in message for k in keywords) |
|
|
|
|
| def keyword_fallback_search(query: str, top_k: int = 5) -> List[Dict]: |
| """ |
| ✅ بحث احتياطي بالكلمات المفتاحية إذا فشل البحث الدلالي |
| يبحث في العناوين والمحتوى مباشرة |
| """ |
| query_words = query.split() |
| results = [] |
| |
| for project in PROJECTS: |
| score = 0 |
| title = project.get("title", "") |
| content = project.get("content", "") |
| |
| for word in query_words: |
| if len(word) < 3: |
| continue |
| if word in title: |
| score += 3 |
| if word in content: |
| score += 1 |
| |
| if score > 0: |
| results.append((score, project)) |
| |
| results.sort(key=lambda x: x[0], reverse=True) |
| return [r[1] for r in results[:top_k]] |
|
|
|
|
| def get_best_matching_project(query: str, matched_projects: List[Dict]) -> Dict: |
| """ |
| ✅ يطلب من LLM اختيار المشروع الأنسب من القائمة |
| بدلاً من الاعتماد على الترتيب العشوائي من ChromaDB |
| """ |
| if len(matched_projects) == 1: |
| return matched_projects[0] |
| |
| projects_list = "\n".join([ |
| f"- رقم {p['id']}: {p['title']}" |
| for p in matched_projects |
| ]) |
| |
| messages = [ |
| SystemMessage(content="أنت محلل. أجب فقط برقم المشروع الأنسب للسؤال، بدون أي كلام إضافي."), |
| HumanMessage(content=f""" |
| السؤال: {query} |
| |
| المشاريع المتاحة: |
| {projects_list} |
| |
| أجب فقط بـ رقم المشروع الأنسب (مثال: 231) |
| """) |
| ] |
| |
| response = llm.invoke(messages).content.strip() |
| |
| |
| import re |
| match = re.search(r'\d+', response) |
| if match: |
| selected_id = int(match.group()) |
| for p in matched_projects: |
| if p.get("id") == selected_id: |
| return p |
| |
| |
| return matched_projects[0] |
|
|
| |
| def chat(message, history): |
| message = message.strip() |
| |
| if not is_project_question(message): |
| return "⚠️ أنا متخصص فقط في مشاريع العتبة العباسية المقدسة. يرجى طرح سؤال متعلق بالمشاريع." |
|
|
| |
| |
| |
| if len(message.split()) <= 6 and "مشاريع" in message and "مشروع" not in message: |
| messages = [ |
| SystemMessage(content="أنت محلل مشاريع. أجب بإحصائية دقيقة."), |
| HumanMessage(content=f""" |
| سؤال: {message} |
| |
| عدد المشاريع الكلي: {PROJECT_COUNT} |
| |
| قائمة العناوين: |
| {ALL_TITLES} |
| """) |
| ] |
| return llm.invoke(messages).content |
|
|
| |
| |
| |
| docs = retriever.invoke(message) |
| |
| |
| matched_projects = [] |
| |
| if docs: |
| |
| for doc in docs: |
| pid = doc.metadata.get("id") |
| if pid and pid in PROJECTS_BY_ID: |
| matched_projects.append(PROJECTS_BY_ID[pid]) |
| |
| if not matched_projects: |
| print(f"[DEBUG] البحث الدلالي لم يجد نتائج، جاري البحث الاحتياطي...") |
| matched_projects = keyword_fallback_search(message) |
| |
| if not matched_projects: |
| return "لا توجد معلومات كافية في قاعدة بيانات المشاريع." |
|
|
| |
| if docs: |
| context = "\n\n".join(doc.page_content for doc in docs) |
| else: |
| context = "\n\n".join( |
| f"عنوان: {p['title']}\nتفاصيل: {p['content']}" |
| for p in matched_projects |
| ) |
|
|
| |
| |
| |
| messages = [ |
| SystemMessage(content="أنت مساعد متخصص في مشاريع العتبة العباسية. أجب بدقة وتفصيل."), |
| HumanMessage(content=f""" |
| سؤال المستخدم: |
| {message} |
| |
| محتوى المشاريع ذات الصلة: |
| {context} |
| """) |
| ] |
|
|
| answer = llm.invoke(messages).content |
|
|
| |
| |
| |
| best_project = get_best_matching_project(message, matched_projects) |
|
|
| |
|
|
| if best_project: |
| answer += "\n\n---\n" |
| answer += f"### 📌 رقم المشروع: {best_project.get('id')}\n" |
| answer += f"### 🏷️ العنوان: {best_project.get('title')}\n" |
|
|
| if best_project.get("image"): |
| answer += f"\n### 📷 صورة المشروع:\n\n" |
|
|
| if best_project.get("url"): |
| answer += f"\n### 🔗 رابط المشروع:\n[{best_project['url']}]({best_project['url']})\n" |
|
|
| return answer |
|
|
|
|
| |
| INITIAL_MESSAGE = """ |
| مرحبًا 👋 |
| اسألني عن أي مشروع أو اطلب تحليل عام للمشاريع. |
| """ |
|
|
| def respond(message, history): |
| if history is None: |
| history = [] |
| history.append({"role": "user", "content": message}) |
| bot_reply = chat(message, history) |
| history.append({"role": "assistant", "content": bot_reply}) |
| return history |
|
|
|
|
| with gr.Blocks(title="🕌 مساعد المشاريع") as demo: |
| gr.Markdown("# 🕌 مساعد مشاريع العتبة العباسية") |
| chatbot = gr.Chatbot( |
| value=[{"role": "assistant", "content": INITIAL_MESSAGE}], |
| height=600, |
| type="messages" |
| ) |
| msg = gr.Textbox(placeholder="اكتب سؤالك هنا...") |
| msg.submit(respond, [msg, chatbot], chatbot) |
|
|
| if __name__ == "__main__": |
| demo.launch() |