Spaces:
Runtime error
Runtime error
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Saudi Legal AI — Production Version v8.2
|
| 3 |
Gemini-first · Groq fallback · FastAPI + HTML frontend
|
| 4 |
"""
|
| 5 |
import os
|
|
@@ -25,7 +25,6 @@ from langchain_community.vectorstores import Chroma
|
|
| 25 |
from langchain_community.embeddings import SentenceTransformerEmbeddings
|
| 26 |
from datasets import load_dataset
|
| 27 |
from rank_bm25 import BM25Okapi
|
| 28 |
-
from rapidfuzz import fuzz, process as fuzz_proc
|
| 29 |
import chromadb
|
| 30 |
|
| 31 |
# ══════════════════════════════════════════════════════════
|
|
@@ -49,11 +48,9 @@ GEMINI_AVAILABLE: bool = False
|
|
| 49 |
gemini_clients: dict = {}
|
| 50 |
groq_client: Optional[Groq] = None
|
| 51 |
|
| 52 |
-
request_log
|
| 53 |
-
api_stats
|
| 54 |
-
user_ips
|
| 55 |
-
_trans_cache = {}
|
| 56 |
-
_legal_cache = {}
|
| 57 |
_answer_cache = {}
|
| 58 |
|
| 59 |
GEMINI_PRIORITY = [
|
|
@@ -95,7 +92,6 @@ COLLOQUIAL = {
|
|
| 95 |
"فين":"أين","مين":"من","ليه":"لماذا","عندي":"لدي",
|
| 96 |
"اشتغلت":"عملت","فصلوني":"تم فصلي","طردوني":"تم فصلي",
|
| 97 |
"اتطردت":"تم فصلي","مش":"لا","كمان":"أيضاً","زي":"مثل",
|
| 98 |
-
"اللازم":"يجب","المفروض":"يجب",
|
| 99 |
"مكافاه":"مكافأة","مكافأه":"مكافأة","مكافاة":"مكافأة",
|
| 100 |
"رخصه":"رخصة","مزاوله":"مزاولة","المحاماه":"المحاماة",
|
| 101 |
"ليا":"لي","هل ليا":"هل يحق لي",
|
|
@@ -109,25 +105,10 @@ COLLOQUIAL = {
|
|
| 109 |
"اتفصلت":"تم فصلي", "هستقيل":"استقالة", "استقيل":"استقالة", "هسيب الشغل":"استقالة"
|
| 110 |
}
|
| 111 |
|
| 112 |
-
ENGLISH_TO_ARABIC = {
|
| 113 |
-
"conditions for lawyer license": "شروط مزاولة مهنة المحاماة",
|
| 114 |
-
"what is the penalty": "ما هي عقوبة",
|
| 115 |
-
"what are my rights": "ما هي حقوقي",
|
| 116 |
-
"how is calculated": "كيف تحسب",
|
| 117 |
-
"i was dismissed": "تم فصلي",
|
| 118 |
-
"wrongful termination": "فصل تعسفي",
|
| 119 |
-
"end of service": "مكافأة نهاية الخدمة",
|
| 120 |
-
"money laundering": "غسل الأموال",
|
| 121 |
-
"cybercrime": "الجرائم المعلوماتية",
|
| 122 |
-
"working hours": "ساعات العمل",
|
| 123 |
-
"annual leave": "الإجازة السنوية",
|
| 124 |
-
"maternity leave": "إجازة الأمومة",
|
| 125 |
-
"labor law": "نظام العمل",
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
LAW_KEYWORDS = {
|
| 129 |
"توثيق":"نظام التوثيق","كاتب عدل":"نظام التوثيق","موثق":"نظام التوثيق",
|
| 130 |
"محاماة":"نظام المحاماة","محامي":"نظام المحاماة","مزاولة مهنة":"نظام المحاماة",
|
|
|
|
| 131 |
"إفلاس":"نظام الإفلاس","إعسار":"نظام الإفلاس",
|
| 132 |
"تحكيم":"نظام التحكيم",
|
| 133 |
"إثبات":"نظام الإثبات","شاهد":"نظام الإثبات",
|
|
@@ -136,6 +117,7 @@ LAW_KEYWORDS = {
|
|
| 136 |
"توقيف":"نظام الإجراءات الجزائية","تهمة":"نظام الإجراءات الجزائية",
|
| 137 |
"محتجز":"نظام الإجراءات الجزائية","جريمة":"نظام الإجراءات الجزائية",
|
| 138 |
"قبض":"نظام الإجراءات الجزائية","قتل":"نظام الإجراءات الجزائية","قضية":"نظام الإجراءات الجزائية",
|
|
|
|
| 139 |
"زواج":"نظام الأحوال الشخصية","طلاق":"نظام الأحوال الشخصية",
|
| 140 |
"نفقة":"نظام الأحوال الشخصية","حضانة":"نظام الأحوال الشخصية","خلع":"نظام الأحوال الشخصية",
|
| 141 |
"عقار":"نظام التسجيل العيني للعقار",
|
|
@@ -173,12 +155,14 @@ PRACTICAL_PATTERNS = {
|
|
| 173 |
QUERY_EXPANSION = {
|
| 174 |
"مكافأة نهاية الخدمة": "يستحق العامل مكافأة عن مدة خدمته نصف شهر سنة",
|
| 175 |
"فصلوني": "إنهاء عقد العمل تعسفي تعويض فصل",
|
|
|
|
| 176 |
"حقوقي": "يستحق العامل مكافأة تعويض نظام العمل",
|
| 177 |
"ساعات العمل": "لا يجوز تشغيل العامل أكثر من ثماني ساعات يومياً",
|
|
|
|
| 178 |
"اتهمت": "حق المتهم في محامٍ أثناء التحقيق الجزائي",
|
|
|
|
| 179 |
"شروط مزاولة المحاماة":"يشترط فيمن يزاول المحاماة سعودي جدول المحامين",
|
| 180 |
"أركان العقد": "أركان العقد الإيجاب والقبول والمحل",
|
| 181 |
-
"عقوبات غسل الأموال": "يعاقب على غسل الأموال سجن غرامة",
|
| 182 |
}
|
| 183 |
|
| 184 |
STOP_WORDS = {"من","في","على","إلى","عن","مع","هي","هو","ما","لا","أن","إن","كل","هذا","ذلك","التي","الذي","هذه","تلك","قد","إذا","إذ"}
|
|
@@ -262,7 +246,11 @@ async def startup():
|
|
| 262 |
print(f" ⚠️ HF load failed: {e}")
|
| 263 |
|
| 264 |
try:
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
train_raw = json.load(f)
|
| 267 |
|
| 268 |
added_from_json = 0
|
|
@@ -281,7 +269,8 @@ async def startup():
|
|
| 281 |
law_name = subj.split("/")[0].strip()
|
| 282 |
law_name = re.sub(r"\(.*?\)", "", law_name).strip()
|
| 283 |
hf_items.append({
|
| 284 |
-
"text": text
|
|
|
|
| 285 |
"law_name": law_name, "law_type": "نظام", "_src": "train.json",
|
| 286 |
"is_exec_reg": 1 if "لائحة" in law_name else 0
|
| 287 |
})
|
|
@@ -309,9 +298,10 @@ async def startup():
|
|
| 309 |
for item in hf_items if len(item.get("text","")) > 30
|
| 310 |
]
|
| 311 |
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
chunks
|
|
|
|
| 315 |
|
| 316 |
chroma_client = chromadb.Client()
|
| 317 |
vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, client=chroma_client, collection_name="saudi_legal_v8")
|
|
@@ -356,7 +346,6 @@ def detect_law(question):
|
|
| 356 |
return None
|
| 357 |
|
| 358 |
def fast_rewrite(question):
|
| 359 |
-
# Fast rewrite without LLM (Saves 3 seconds!)
|
| 360 |
queries = [question]
|
| 361 |
for pat, exp in QUERY_EXPANSION.items():
|
| 362 |
if pat in question: queries.append(exp); break
|
|
@@ -431,28 +420,24 @@ def post_process(answer, docs):
|
|
| 431 |
law = docs[0].metadata.get("law_name","")
|
| 432 |
art = docs[0].metadata.get("article_number","")
|
| 433 |
if law and art: answer = f"📋 المرجع: {law} — {art}\n\n{answer}"
|
| 434 |
-
|
| 435 |
-
clean = [l for l in lines if
|
| 436 |
-
sum(1 for c in l if "\u0600"<=c<="\u06ff") / max(len(l.replace(" ","")),1) > 0.25
|
| 437 |
-
or any(s in l for s in ["📋","✅","📝","⚠️","•","-","─"])]
|
| 438 |
-
return "\n".join(clean).strip()
|
| 439 |
|
| 440 |
# ══════════════════════════════════════════════════════════
|
| 441 |
# System Prompts
|
| 442 |
# ══════════════════════════════════════════════════════════
|
| 443 |
-
SYSTEM_PROMPT = """أنت مسا
|
| 444 |
|
| 445 |
-
نطاقك: وزارة العدل، نظام العمل، الإجراءات الجزائية، ال
|
| 446 |
|
| 447 |
صيغة الإجابة الإلزامية:
|
| 448 |
📋 المرجع: [اسم النظام] — [رقم المادة]
|
| 449 |
-
✅ الإجابة: [إجابة
|
| 450 |
-
📝 التفاصيل: [شرح
|
| 451 |
|
| 452 |
قواعد:
|
| 453 |
١. العربية الفصحى حصراً.
|
| 454 |
٢. اذكر النظام والمادة دائماً.
|
| 455 |
-
٣.
|
| 456 |
٤. ابدأ بـ 📋 مباشرة."""
|
| 457 |
|
| 458 |
OUT_OF_SCOPE = """أنا مساعد قانوني متخصص في الأنظمة السعودية.
|
|
@@ -542,7 +527,7 @@ def ask_legal(question: str) -> dict:
|
|
| 542 |
elif has_anti and any(w in question for w in ["فلوس", "دين", "سلف"]):
|
| 543 |
target_law = "نظام المعاملات المدنية"
|
| 544 |
|
| 545 |
-
#
|
| 546 |
seen, result_docs = set(), []
|
| 547 |
def add_docs(new_docs):
|
| 548 |
for d in new_docs:
|
|
@@ -583,8 +568,10 @@ def ask_legal(question: str) -> dict:
|
|
| 583 |
return {"answer":"الخدمة مشغولة — يرجى المحاولة مجدداً.",
|
| 584 |
"sources":[],"coverage":0,"model":"unavailable","normalized":question}
|
| 585 |
|
|
|
|
|
|
|
| 586 |
result = {
|
| 587 |
-
"answer":
|
| 588 |
"sources": [{"law":d.metadata.get("law_name",""),"article":d.metadata.get("article_number","")} for d in final[:3]],
|
| 589 |
"coverage": round(cov*100) if target_law else round(cov*50),
|
| 590 |
"model": model_used,
|
|
@@ -1175,5 +1162,4 @@ async def ask_endpoint(req: QuestionRequest, request: Request):
|
|
| 1175 |
|
| 1176 |
if __name__ == "__main__":
|
| 1177 |
import uvicorn
|
| 1178 |
-
# ── استخدام Port 7860 المطلوب من Hugging Face Spaces ──
|
| 1179 |
uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=False)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Saudi Legal AI — Production Version v8.2
|
| 3 |
Gemini-first · Groq fallback · FastAPI + HTML frontend
|
| 4 |
"""
|
| 5 |
import os
|
|
|
|
| 25 |
from langchain_community.embeddings import SentenceTransformerEmbeddings
|
| 26 |
from datasets import load_dataset
|
| 27 |
from rank_bm25 import BM25Okapi
|
|
|
|
| 28 |
import chromadb
|
| 29 |
|
| 30 |
# ══════════════════════════════════════════════════════════
|
|
|
|
| 48 |
gemini_clients: dict = {}
|
| 49 |
groq_client: Optional[Groq] = None
|
| 50 |
|
| 51 |
+
request_log = deque(maxlen=500)
|
| 52 |
+
api_stats = defaultdict(int)
|
| 53 |
+
user_ips = defaultdict(list)
|
|
|
|
|
|
|
| 54 |
_answer_cache = {}
|
| 55 |
|
| 56 |
GEMINI_PRIORITY = [
|
|
|
|
| 92 |
"فين":"أين","مين":"من","ليه":"لماذا","عندي":"لدي",
|
| 93 |
"اشتغلت":"عملت","فصلوني":"تم فصلي","طردوني":"تم فصلي",
|
| 94 |
"اتطردت":"تم فصلي","مش":"لا","كمان":"أيضاً","زي":"مثل",
|
|
|
|
| 95 |
"مكافاه":"مكافأة","مكافأه":"مكافأة","مكافاة":"مكافأة",
|
| 96 |
"رخصه":"رخصة","مزاوله":"مزاولة","المحاماه":"المحاماة",
|
| 97 |
"ليا":"لي","هل ليا":"هل يحق لي",
|
|
|
|
| 105 |
"اتفصلت":"تم فصلي", "هستقيل":"استقالة", "استقيل":"استقالة", "هسيب الشغل":"استقالة"
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
LAW_KEYWORDS = {
|
| 109 |
"توثيق":"نظام التوثيق","كاتب عدل":"نظام التوثيق","موثق":"نظام التوثيق",
|
| 110 |
"محاماة":"نظام المحاماة","محامي":"نظام المحاماة","مزاولة مهنة":"نظام المحاماة",
|
| 111 |
+
"ترخيص محاماة":"نظام المحاماة",
|
| 112 |
"إفلاس":"نظام الإفلاس","إعسار":"نظام الإفلاس",
|
| 113 |
"تحكيم":"نظام التحكيم",
|
| 114 |
"إثبات":"نظام الإثبات","شاهد":"نظام الإثبات",
|
|
|
|
| 117 |
"توقيف":"نظام الإجراءات الجزائية","تهمة":"نظام الإجراءات الجزائية",
|
| 118 |
"محتجز":"نظام الإجراءات الجزائية","جريمة":"نظام الإجراءات الجزائية",
|
| 119 |
"قبض":"نظام الإجراءات الجزائية","قتل":"نظام الإجراءات الجزائية","قضية":"نظام الإجراءات الجزائية",
|
| 120 |
+
"عليا قضية":"نظام الإجراءات الجزائية",
|
| 121 |
"زواج":"نظام الأحوال الشخصية","طلاق":"نظام الأحوال الشخصية",
|
| 122 |
"نفقة":"نظام الأحوال الشخصية","حضانة":"نظام الأحوال الشخصية","خلع":"نظام الأحوال الشخصية",
|
| 123 |
"عقار":"نظام التسجيل العيني للعقار",
|
|
|
|
| 155 |
QUERY_EXPANSION = {
|
| 156 |
"مكافأة نهاية الخدمة": "يستحق العامل مكافأة عن مدة خدمته نصف شهر سنة",
|
| 157 |
"فصلوني": "إنهاء عقد العمل تعسفي تعويض فصل",
|
| 158 |
+
"اتطردت": "تم فصلي تعويض فصل تعسفي مكافأة نهاية الخدمة",
|
| 159 |
"حقوقي": "يستحق العامل مكافأة تعويض نظام العمل",
|
| 160 |
"ساعات العمل": "لا يجوز تشغيل العامل أكثر من ثماني ساعات يومياً",
|
| 161 |
+
"شهادة خبرة": "يلتزم صاحب العمل بإعطاء شهادة خبرة عند انتهاء العقد",
|
| 162 |
"اتهمت": "حق المتهم في محامٍ أثناء التحقيق الجزائي",
|
| 163 |
+
"حقوق الموقوف": "يحق للموقوف الاتصال بمحاميه وذويه عند التوقيف",
|
| 164 |
"شروط مزاولة المحاماة":"يشترط فيمن يزاول المحاماة سعودي جدول المحامين",
|
| 165 |
"أركان العقد": "أركان العقد الإيجاب والقبول والمحل",
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
STOP_WORDS = {"من","في","على","إلى","عن","مع","هي","هو","ما","لا","أن","إن","كل","هذا","ذلك","التي","الذي","هذه","تلك","قد","إذا","إذ"}
|
|
|
|
| 246 |
print(f" ⚠️ HF load failed: {e}")
|
| 247 |
|
| 248 |
try:
|
| 249 |
+
# حل مشكلة المسار المطلق لملف train.json
|
| 250 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 251 |
+
json_path = os.path.join(base_dir, "train.json")
|
| 252 |
+
|
| 253 |
+
with open(json_path, "r", encoding="utf-8") as f:
|
| 254 |
train_raw = json.load(f)
|
| 255 |
|
| 256 |
added_from_json = 0
|
|
|
|
| 269 |
law_name = subj.split("/")[0].strip()
|
| 270 |
law_name = re.sub(r"\(.*?\)", "", law_name).strip()
|
| 271 |
hf_items.append({
|
| 272 |
+
"text": text, # أخذ المادة كاملة دون القص
|
| 273 |
+
"article_number": "نص قانوني",
|
| 274 |
"law_name": law_name, "law_type": "نظام", "_src": "train.json",
|
| 275 |
"is_exec_reg": 1 if "لائحة" in law_name else 0
|
| 276 |
})
|
|
|
|
| 298 |
for item in hf_items if len(item.get("text","")) > 30
|
| 299 |
]
|
| 300 |
|
| 301 |
+
# تكبير الحجم لعدم قص الكلمات في منتصفها
|
| 302 |
+
splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=300, separators=["\nالمادة ","\n\n","\n","."," "])
|
| 303 |
+
chunks = [d for d in docs_lc if len(d.page_content) <= 2000]
|
| 304 |
+
chunks += splitter.split_documents([d for d in docs_lc if len(d.page_content) > 2000])
|
| 305 |
|
| 306 |
chroma_client = chromadb.Client()
|
| 307 |
vectorstore = Chroma.from_documents(documents=chunks, embedding=embeddings, client=chroma_client, collection_name="saudi_legal_v8")
|
|
|
|
| 346 |
return None
|
| 347 |
|
| 348 |
def fast_rewrite(question):
|
|
|
|
| 349 |
queries = [question]
|
| 350 |
for pat, exp in QUERY_EXPANSION.items():
|
| 351 |
if pat in question: queries.append(exp); break
|
|
|
|
| 420 |
law = docs[0].metadata.get("law_name","")
|
| 421 |
art = docs[0].metadata.get("article_number","")
|
| 422 |
if law and art: answer = f"📋 المرجع: {law} — {art}\n\n{answer}"
|
| 423 |
+
return answer # بدون أي حذف للسطور
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
|
| 425 |
# ══════════════════════════════════════════════════════════
|
| 426 |
# System Prompts
|
| 427 |
# ══════════════════════════════════════════════════════════
|
| 428 |
+
SYSTEM_PROMPT = """أنت مستشار قانوني سعودي محترف وشامل في إجاباتك.
|
| 429 |
|
| 430 |
+
نطاقك: وزارة العدل، نظام العمل، الإجراءات الجزائية، المعاملات المدنية، الجرائم المعلوماتية.
|
| 431 |
|
| 432 |
صيغة الإجابة الإلزامية:
|
| 433 |
📋 المرجع: [اسم النظام] — [رقم المادة]
|
| 434 |
+
✅ الإجابة: [إجابة واضحة وشاملة للسؤال. استخدم نعم/لا فقط إذا كان السؤال يبدأ بـ "هل"]
|
| 435 |
+
📝 التفاصيل: [اشرح هنا بالتفصيل الممل والواضح بناءً على النصوص المرفقة. اكتب فقرة كاملة مترابطة ولا تقم أبداً ببتر الجمل]
|
| 436 |
|
| 437 |
قواعد:
|
| 438 |
١. العربية الفصحى حصراً.
|
| 439 |
٢. اذكر النظام والمادة دائماً.
|
| 440 |
+
٣. اشرح براحتك وبشكل مفصل جداً لمساعدة السائل.
|
| 441 |
٤. ابدأ بـ 📋 مباشرة."""
|
| 442 |
|
| 443 |
OUT_OF_SCOPE = """أنا مساعد قانوني متخصص في الأنظمة السعودية.
|
|
|
|
| 527 |
elif has_anti and any(w in question for w in ["فلوس", "دين", "سلف"]):
|
| 528 |
target_law = "نظام المعاملات المدنية"
|
| 529 |
|
| 530 |
+
# Retrieval
|
| 531 |
seen, result_docs = set(), []
|
| 532 |
def add_docs(new_docs):
|
| 533 |
for d in new_docs:
|
|
|
|
| 568 |
return {"answer":"الخدمة مشغولة — يرجى المحاولة مجدداً.",
|
| 569 |
"sources":[],"coverage":0,"model":"unavailable","normalized":question}
|
| 570 |
|
| 571 |
+
ans = post_process(ans, final)
|
| 572 |
+
|
| 573 |
result = {
|
| 574 |
+
"answer": ans,
|
| 575 |
"sources": [{"law":d.metadata.get("law_name",""),"article":d.metadata.get("article_number","")} for d in final[:3]],
|
| 576 |
"coverage": round(cov*100) if target_law else round(cov*50),
|
| 577 |
"model": model_used,
|
|
|
|
| 1162 |
|
| 1163 |
if __name__ == "__main__":
|
| 1164 |
import uvicorn
|
|
|
|
| 1165 |
uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=False)
|