Spaces:
Sleeping
Sleeping
| import os | |
| import gradio as gr | |
| from langchain_huggingface import HuggingFaceEmbeddings | |
| from langchain_core.prompts import PromptTemplate | |
| from langchain_openai import ChatOpenAI | |
| from langchain_pinecone import PineconeVectorStore | |
| from pinecone import Pinecone | |
| from sentence_transformers import CrossEncoder | |
| # =============================== | |
| # Hugging Face Spaces-ready Seerah Q&A (Gradio) | |
| # =============================== | |
| # --------- THEME & CSS ---------- | |
| THEME = gr.themes.Soft( | |
| primary_hue="emerald", | |
| radius_size=gr.themes.sizes.radius_lg, | |
| text_size=gr.themes.sizes.text_md, | |
| ) | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Kufi+Arabic:wght@400;600;700&display=swap'); | |
| html { | |
| color-scheme: light !important; | |
| } | |
| body { | |
| background: #1e293b; | |
| font-family: 'Noto Kufi Arabic', sans-serif; | |
| color: #0f172a; | |
| line-height: 1.8; | |
| } | |
| *, *::before, *::after { | |
| color: inherit !important; | |
| -webkit-text-fill-color: currentColor !important; | |
| } | |
| .app-card { | |
| max-width: 950px; | |
| margin: 40px auto; | |
| padding: 30px; | |
| background: #ffffff; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 18px; | |
| box-shadow: 0 6px 20px rgba(0,0,0,0.06); | |
| direction: rtl; | |
| text-align: right; | |
| } | |
| h1, h2, h3 { | |
| font-family: 'Noto Kufi Arabic', sans-serif; | |
| font-weight: 700; | |
| color: #0f172a; | |
| margin-bottom: 12px; | |
| } | |
| p, label, textarea { | |
| font-family: 'Noto Kufi Arabic', sans-serif; | |
| font-size: 16px; | |
| direction: rtl; | |
| text-align: right; | |
| } | |
| button.primary { | |
| border-radius: 12px; | |
| background: #10b981; | |
| color: white; | |
| font-weight: 600; | |
| font-family: 'Noto Kufi Arabic', sans-serif; | |
| transition: transform .08s ease, box-shadow .2s ease; | |
| } | |
| button.primary:hover { | |
| background: #059669; | |
| transform: translateY(-1px); | |
| box-shadow: 0 6px 16px rgba(16,185,129,.25); | |
| } | |
| textarea { | |
| border-radius: 12px; | |
| border: 1px solid #d1d5db; | |
| padding: 12px; | |
| background: #fdfdfd; | |
| min-height: 200px; | |
| } | |
| """ | |
| # --------- PIPELINE ---------- | |
| PIPELINE = { | |
| "ready": False, | |
| "error": None, | |
| "vectorstore": None, | |
| "llm": None, | |
| "prompt": None, | |
| } | |
| def _init_pipeline(): | |
| if PIPELINE["ready"] or PIPELINE["error"]: | |
| return | |
| try: | |
| openai_api_key = os.getenv("OPENAI_API_KEY", "").strip() | |
| pinecone_api_key = os.getenv("PINECONE_API_KEY", "").strip() | |
| index_name = os.getenv("PINECONE_INDEX_NAME", "seerah").strip() | |
| if not openai_api_key: | |
| raise RuntimeError("OPENAI_API_KEY is missing.") | |
| if not pinecone_api_key: | |
| raise RuntimeError("PINECONE_API_KEY is missing.") | |
| embedding = HuggingFaceEmbeddings(model_name="omarelshehy/Arabic-STS-Matryoshka-V2") | |
| pc = Pinecone(api_key=pinecone_api_key) | |
| if index_name not in [idx["name"] for idx in pc.list_indexes()]: | |
| raise RuntimeError(f"Pinecone index '{index_name}' not found.") | |
| vectorstore = PineconeVectorStore.from_existing_index( | |
| index_name=index_name, | |
| embedding=embedding, | |
| namespace="seerah" | |
| ) | |
| llm = ChatOpenAI(model="gpt-4o", temperature=0, openai_api_key=openai_api_key) | |
| prompt = PromptTemplate.from_template(""" | |
| أنت ذكاءٌ اصطناعي تمثل مؤلف كتاب "الرحيق المختوم"، وتجيب كما لو كنت الشيخ صفيّ الرحمن المباركفوري. | |
| القواعد العليا: | |
| - أجب فقط من محتوى "الرحيق المختوم" دون أي إضافة خارجية. | |
| - ابحث في كل الكتاب وأرجع المعلومة الصحيحة ولا تقول هذا ليس في الكتاب إلا وأنت متأكد- | |
| - اذكر رقم الصفحة لكل معلومة بدقة. إذا لم تتأكد من الصفحة فلا تذكرها ولا تخمن. | |
| - صحّح أي خطأ يورده المستخدم في الأسماء/الصفات/الأحداث وفق ما في الكتاب. | |
| - أحداث السيرة كانت في القرن السادس. فلا يوجد أسلحة نارية ولا ألغام | |
| - أقرب معلومة هي الأقرب للصحة | |
| قواعد الاصطلاح واللغة: | |
| - استخدم الاشتقاق: "أول الغزوات" = "أول غزوة". | |
| - لا تفرّق بين المفرد والجمع إن كان المقصود واحدًا. | |
| - اعتبر السؤال كأنه ناتج بحث أجريته أنت على الكتاب. | |
| - ابحث بالمعنى لا باللفظ فقط مع الالتزام بالنص. | |
| - تأكد من أن الضمائر دقيقة. | |
| - إذا لم يُذكر قيد أو تخصيص (مثل: "من النساء" أو "من الموالي") فافترض أن المقصود هو الأول على الإطلاق.. | |
| - إذا قُيِّد السؤال بـ"من الرجال" فلا تذكر إناثًا مطلقًا، وإذا قُيِّد بـ"من النساء" فلا تذكر رجالًا مطلقًا. | |
| قواعد المصطلحات: | |
| - الشهيد فقط الذي سماه الكتاب شهيد. | |
| - يُسمّى المسلم المقتول "شهيدًا" كما استعمله المؤلف فقط، ولا يُطلق على غير المسلمين. | |
| - يُسمّى المشرك المقتول "قتيلاً" ولا يسمى شهيدا أبدا. | |
| - الغزوة هي ما سمّاه المؤلف "غزوة"، والسرية هي ما سمّاه "سرية"، وكلاهما يدخل تحت "معركة" عند الإجمال. | |
| التقييدات: | |
| - إذا كان السؤال خارج السيرة النبوية فاذكر أنه خارج موضوع الكتاب. | |
| السؤال: | |
| {question} | |
| مقاطع من الكتاب مؤشرة بالصفحات: | |
| {context} | |
| """) | |
| PIPELINE.update({ | |
| "ready": True, | |
| "vectorstore": vectorstore, | |
| "llm": llm, | |
| "prompt": prompt, | |
| }) | |
| except Exception as e: | |
| PIPELINE["error"] = str(e) | |
| # --------- ANSWER FUNCTION ---------- | |
| def answer(user_input: str): | |
| question = (user_input or "").strip() | |
| question = question.replace("أ", "ا").replace("إ", "ا").replace("آ", "ا").replace("ة", "ه") | |
| _init_pipeline() | |
| if PIPELINE["error"]: | |
| return f"⚠️ لا يمكن تشغيل الخدمة الآن. السبب: {PIPELINE['error']}" | |
| if not PIPELINE["ready"]: | |
| return "⏳ جارٍ التهيئة… أعد المحاولة بعد لحظات." | |
| vectorstore = PIPELINE["vectorstore"] | |
| llm = PIPELINE["llm"] | |
| prompt = PIPELINE["prompt"] | |
| try: | |
| # ✅ Use similarity_search (same as your working test) | |
| docs = vectorstore.similarity_search(question, k=50, namespace="seerah") | |
| except Exception as e: | |
| return f"⚠️ خطأ أثناء الاسترجاع من Pinecone: {e}" | |
| context = "\n".join([ | |
| f"[صفحة {d.metadata.get('page','?')}] {d.page_content.strip()}" | |
| for d in docs | |
| ]) | |
| try: | |
| filled = prompt.format(question=question, context=context) | |
| result = llm.invoke(filled) | |
| return f"{result.content}" | |
| except Exception as e: | |
| return f"⚠️ خطأ أثناء توليد الإجابة: {e}" | |
| # --------- GRADIO UI ---------- | |
| with gr.Blocks(theme=THEME, css=CSS, title="نموذج ذكاء اصطناعي يجيب من كتاب الرحيق المختوم") as interface: | |
| gr.HTML(""" | |
| <div class='app-card' style="text-align:center;"> | |
| <img src="https://huggingface.co/spaces/faris1419124/Seerah_/resolve/main/sira_naskh.png" alt="سيرة" | |
| style="max-width:220px; margin:0 auto 15px; display:block;"> | |
| <p>اكتب استفسارك وسيجيب الذكاء الاصطناعي اعتمادًا على كتاب الرحيق المختوم.</p> | |
| <a href='https://shamela.ws/book/9820/2#p1' target='_blank' style='color:#0ea5e9;'> | |
| رابط الكتاب – نسخة دار الفكر | |
| </a> | |
| </div> | |
| """) | |
| with gr.Group(): | |
| with gr.Row(): | |
| gr.Markdown("#### ✍️ اكتب سؤالك ثم اضغط **إرسال**") | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=3): | |
| out = gr.Textbox(label="💡 الجواب", rtl=True, elem_id="output") | |
| with gr.Column(scale=3): | |
| inp = gr.Textbox( | |
| label="اكتب سؤالك هنا", | |
| placeholder="مثال: من أول من أسلم؟ / متى كانت غزوة بدر؟", | |
| lines=6, | |
| rtl=True, | |
| autofocus=True, | |
| ) | |
| with gr.Row(): | |
| send = gr.Button("إرسال", variant="primary", elem_classes=["primary"], scale=1) | |
| clear = gr.ClearButton(components=[inp], value="✧ امسح") | |
| gr.Examples([ | |
| ["اذكر أول من أسلم من الرجال"], | |
| ["متى كانت غزوة أحد؟ اذكر الصفحة"], | |
| ], inputs=[inp], label="أمثلة جاهزة") | |
| gr.HTML(""" | |
| <div class='footer'> | |
| مبنيّ باستخدام Gradio — بخط <b>Noto Kufi Arabic</b>. | |
| </div> | |
| """) | |
| send.click(fn=answer, inputs=inp, outputs=out) | |
| inp.submit(fn=answer, inputs=inp, outputs=out) | |
| if __name__ == "__main__": | |
| print("🔍 Checking Pinecone connection...") | |
| interface.queue().launch() | |