Seerah_ / app.py
faris1419124's picture
Change font color
abf8470
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()