shahad altamimi commited on
Commit
96d14d2
·
1 Parent(s): b1981c6
reverse-dictionary/backend/app/ratings.py DELETED
@@ -1,124 +0,0 @@
1
- # ============================================================
2
- # 📄 الملف: app/ratings.py
3
- # 🎯 الغرض: إدارة سجل التقييمات (حفظ / قراءة / حذف / إحصاء).
4
- #
5
- # كل تقييم يُحفظ كصف في ملف CSV (ratings.csv) بترميز utf-8-sig
6
- # عشان العربية تظهر صح في Excel. هذا الملف يوفّر كل العمليات
7
- # على السجل: إضافة تقييم، جلب السجل كامل، حذف صفوف محدّدة،
8
- # حذف الكل، وحساب ملخّص أداء النماذج.
9
- # ============================================================
10
-
11
- import os
12
- from datetime import datetime
13
-
14
- import pandas as pd
15
-
16
- import config
17
-
18
- # أسماء الأعمدة في ملف التقييمات
19
- COLUMNS = ["التاريخ والوقت", "الاستعلام", "النموذج", "التقييم",
20
- "Threshold", "أفضل كلمة", "أعلى تشابه"]
21
- REQUIRED = ["التاريخ والوقت", "الاستعلام", "النموذج", "التقييم"]
22
-
23
-
24
- # ------------------------------------------------------------
25
- # 💾 حفظ تقييم جديد (إضافة صف للملف)
26
- # ------------------------------------------------------------
27
- def save_rating(query, model_display_name, rating, threshold,
28
- top_lemma=None, top_score=None) -> None:
29
- row = {
30
- "التاريخ والوقت": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
31
- "الاستعلام": query,
32
- "النموذج": model_display_name,
33
- "التقييم": rating,
34
- "Threshold": threshold,
35
- "أفضل كلمة": top_lemma or "",
36
- "أعلى تشابه": round(top_score, 3) if top_score else "",
37
- }
38
- write_header = not config.RATINGS_PATH.exists()
39
- pd.DataFrame([row]).to_csv(
40
- config.RATINGS_PATH, mode="a", header=write_header,
41
- index=False, encoding="utf-8-sig",
42
- )
43
-
44
-
45
- # ------------------------------------------------------------
46
- # 📖 قراءة كل التقييمات (مع تنظيف الملف التالف تلقائيًا)
47
- # ------------------------------------------------------------
48
- def load_ratings() -> pd.DataFrame:
49
- if not config.RATINGS_PATH.exists():
50
- return pd.DataFrame(columns=COLUMNS)
51
- try:
52
- df = pd.read_csv(config.RATINGS_PATH, encoding="utf-8-sig")
53
- # لو الملف ناقص أعمدة أساسية نعتبره تالفًا ونحذفه
54
- if not all(c in df.columns for c in REQUIRED):
55
- config.RATINGS_PATH.unlink()
56
- return pd.DataFrame(columns=COLUMNS)
57
- return df
58
- except Exception:
59
- config.RATINGS_PATH.unlink()
60
- return pd.DataFrame(columns=COLUMNS)
61
-
62
-
63
- # ------------------------------------------------------------
64
- # 🗑️ حذف تقييمات محدّدة (حسب مفاتيح: التاريخ + الاستعلام + النموذج)
65
- # ------------------------------------------------------------
66
- def delete_ratings(keys: list[dict]) -> int:
67
- """keys = [{datetime, query, model}, ...]. يرجّع عدد الصفوف المحذوفة."""
68
- df = load_ratings()
69
- if df.empty:
70
- return 0
71
-
72
- key_tuples = {(k["datetime"], k["query"], k["model"]) for k in keys}
73
- mask = df[["التاريخ والوقت", "الاستعلام", "النموذج"]].apply(tuple, axis=1).isin(key_tuples)
74
- deleted_count = int(mask.sum())
75
-
76
- remaining = df[~mask]
77
- if len(remaining) > 0:
78
- remaining.to_csv(config.RATINGS_PATH, index=False, encoding="utf-8-sig")
79
- elif config.RATINGS_PATH.exists():
80
- config.RATINGS_PATH.unlink()
81
- return deleted_count
82
-
83
-
84
- # ------------------------------------------------------------
85
- # ⚠️ حذف كل التقييمات
86
- # ------------------------------------------------------------
87
- def delete_all_ratings() -> None:
88
- if config.RATINGS_PATH.exists():
89
- config.RATINGS_PATH.unlink()
90
-
91
-
92
- # ------------------------------------------------------------
93
- # 📊 ملخّص أداء النماذج + إحصاءات عامة
94
- # ------------------------------------------------------------
95
- def get_summary() -> dict:
96
- df = load_ratings()
97
- if df.empty:
98
- return {"stats": {}, "performance": []}
99
-
100
- # إحصاءات عامة
101
- stats = {
102
- "total": len(df),
103
- "ممتاز": int((df["التقييم"] == "ممتاز").sum()),
104
- "جيد": int((df["التقييم"] == "جيد").sum()),
105
- "ضعيف": int((df["التقييم"] == "ضعيف").sum()),
106
- }
107
-
108
- # أداء كل نموذج
109
- performance = []
110
- for model_name, group in df.groupby("النموذج"):
111
- excellent = int((group["التقييم"] == "ممتاز").sum())
112
- good = int((group["التقييم"] == "جيد").sum())
113
- weak = int((group["التقييم"] == "ضعيف").sum())
114
- total = excellent + good + weak
115
- performance.append({
116
- "model": model_name,
117
- "ممتاز": excellent,
118
- "جيد": good,
119
- "ضعيف": weak,
120
- "الإجمالي": total,
121
- "نسبة الممتاز": round(excellent / total * 100, 1) if total else 0.0,
122
- })
123
-
124
- return {"stats": stats, "performance": performance}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
reverse-dictionary/backend/app/routes.py DELETED
@@ -1,96 +0,0 @@
1
- # ============================================================
2
- # 📄 الملف: app/routes.py
3
- # 🎯 الغرض: تعريف نقاط النهاية (Endpoints) للـ API.
4
- #
5
- # هذا الملف يربط بين طلبات HTTP القادمة من الـ frontend
6
- # ومنطق المشروع (البحث، التقييمات). كل دالة هنا تمثّل
7
- # "عنوانًا" يستدعيه الـ frontend:
8
- #
9
- # GET /api/models → قائمة النماذج
10
- # POST /api/search → تنفيذ بحث
11
- # GET /api/ratings → جلب كل التقييمات + الملخّص
12
- # POST /api/ratings → حفظ تقييم
13
- # POST /api/ratings/delete → حذف تقييمات محدّدة
14
- # DELETE /api/ratings → حذف كل التقييمات
15
- # ============================================================
16
-
17
- from fastapi import APIRouter
18
-
19
- import config
20
- from app import ratings as ratings_service
21
- from app.search import search_all_models
22
- from app.schemas import (
23
- DeleteRatingsRequest,
24
- ModelInfo,
25
- RatingRequest,
26
- SearchRequest,
27
- SearchResponse,
28
- )
29
-
30
- router = APIRouter(prefix="/api")
31
-
32
-
33
- # ------------------------------------------------------------
34
- # 🤖 قائمة النماذج
35
- # ------------------------------------------------------------
36
- @router.get("/models", response_model=list[ModelInfo])
37
- def get_models():
38
- return [
39
- ModelInfo(model_index=i, model_key=m["key"], display_name=m["display_name"])
40
- for i, m in enumerate(config.MODELS)
41
- ]
42
-
43
-
44
- # ------------------------------------------------------------
45
- # 🔎 البحث
46
- # ------------------------------------------------------------
47
- @router.post("/search", response_model=SearchResponse)
48
- def search(request: SearchRequest):
49
- results = search_all_models(request.query.strip(), request.threshold,
50
- rerank=request.rerank)
51
- return SearchResponse(
52
- query=request.query,
53
- threshold=request.threshold,
54
- results=results,
55
- )
56
-
57
-
58
- # ------------------------------------------------------------
59
- # ⭐ التقييمات
60
- # ------------------------------------------------------------
61
- @router.get("/ratings")
62
- def get_ratings():
63
- df = ratings_service.load_ratings()
64
- summary = ratings_service.get_summary()
65
- return {
66
- "ratings": df.to_dict(orient="records"),
67
- "stats": summary["stats"],
68
- "performance": summary["performance"],
69
- }
70
-
71
-
72
- @router.post("/ratings")
73
- def add_rating(request: RatingRequest):
74
- ratings_service.save_rating(
75
- query=request.query,
76
- model_display_name=request.model_display_name,
77
- rating=request.rating,
78
- threshold=request.threshold,
79
- top_lemma=request.top_lemma,
80
- top_score=request.top_score,
81
- )
82
- return {"status": "ok", "message": "تم حفظ التقييم"}
83
-
84
-
85
- @router.post("/ratings/delete")
86
- def delete_selected(request: DeleteRatingsRequest):
87
- keys = [{"datetime": k.datetime, "query": k.query, "model": k.model}
88
- for k in request.keys]
89
- count = ratings_service.delete_ratings(keys)
90
- return {"status": "ok", "deleted": count}
91
-
92
-
93
- @router.delete("/ratings")
94
- def delete_all():
95
- ratings_service.delete_all_ratings()
96
- return {"status": "ok", "message": "تم حذف جميع التقييمات"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
reverse-dictionary/backend/app/schemas.py DELETED
@@ -1,84 +0,0 @@
1
- # ============================================================
2
- # 📄 الملف: app/schemas.py
3
- # 🎯 الغرض: تعريف أشكال البيانات (Schemas) الداخلة والخارجة من الـ API.
4
- #
5
- # نستخدم Pydantic عشان:
6
- # 1. التحقّق التلقائي من صحة البيانات القادمة من الـ frontend.
7
- # 2. توليد توثيق تلقائي للـ API (صفحة /docs).
8
- # 3. توضيح "العقد" بين الواجهة والخادم: وش يرسل ووش يستقبل.
9
- # ============================================================
10
-
11
- from typing import List, Optional
12
-
13
- from pydantic import BaseModel, Field
14
-
15
-
16
- # ------------------------------------------------------------
17
- # 🔎 طلبات واستجابات البحث
18
- # ------------------------------------------------------------
19
- class SearchRequest(BaseModel):
20
- """طلب البحث القادم من الواجهة."""
21
- query: str = Field(..., description="التعريف أو النص المُراد البحث عنه")
22
- threshold: float = Field(0.70, ge=0.0, le=1.0, description="حد التشابه الأدنى")
23
- rerank: bool = Field(False, description="تفعيل إعادة الترتيب بالـ Cross-Encoder")
24
-
25
-
26
- class ResultItem(BaseModel):
27
- """نتيجة واحدة (كلمة) داخل قائمة نتائج نموذج."""
28
- lemma: str
29
- definition: str
30
- example: str = ""
31
- pos: str = ""
32
- score: float
33
-
34
-
35
- class ModelResult(BaseModel):
36
- """نتائج نموذج واحد كاملة."""
37
- model_index: int
38
- model_key: str
39
- display_name: str
40
- items: List[ResultItem]
41
- top_lemma: Optional[str] = None
42
- top_score: Optional[float] = None
43
-
44
-
45
- class SearchResponse(BaseModel):
46
- """الاستجابة الكاملة للبحث (نتائج النماذج الثلاثة)."""
47
- query: str
48
- threshold: float
49
- results: List[ModelResult]
50
-
51
-
52
- # ------------------------------------------------------------
53
- # ⭐ طلبات واستجابات التقييمات
54
- # ------------------------------------------------------------
55
- class RatingRequest(BaseModel):
56
- """طلب حفظ تقييم لنتيجة نموذج."""
57
- query: str
58
- model_display_name: str
59
- rating: str = Field(..., description="ممتاز | جيد | ضعيف")
60
- threshold: float
61
- top_lemma: Optional[str] = ""
62
- top_score: Optional[float] = None
63
-
64
-
65
- class RatingKey(BaseModel):
66
- """مفتاح يُعرّف صف تقييم بشكل فريد (للحذف)."""
67
- datetime: str
68
- query: str
69
- model: str
70
-
71
-
72
- class DeleteRatingsRequest(BaseModel):
73
- """طلب حذف مجموعة تقييمات محدّدة."""
74
- keys: List[RatingKey]
75
-
76
-
77
- # ------------------------------------------------------------
78
- # 🤖 معلومات النماذج
79
- # ------------------------------------------------------------
80
- class ModelInfo(BaseModel):
81
- """معلومات نموذج تُعرض للواجهة (بدون كشف المسارات الداخلية)."""
82
- model_index: int
83
- model_key: str
84
- display_name: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
reverse-dictionary/backend/main.py DELETED
@@ -1,66 +0,0 @@
1
- # ============================================================
2
- # 📄 الملف: main.py
3
- # 🎯 الغرض: نقطة تشغيل الخادم (Backend) — تطبيق FastAPI.
4
- #
5
- # هذا هو الملف اللي تشغّله لرفع الـ backend. مسؤوليّاته:
6
- # 1. إنشاء تطبيق FastAPI.
7
- # 2. تفعيل CORS (يسمح للـ frontend باستدعاء الـ API).
8
- # 3. ربط نقاط النهاية (من app/routes.py).
9
- # 4. تحميل النماذج مسبقًا عند الإقلاع (warmup).
10
- #
11
- # طريقة التشغيل:
12
- # cd backend
13
- # uvicorn main:app --reload --port 8000
14
- #
15
- # توثيق الـ API يكون تلقائيًا على: http://localhost:8000/docs
16
- # ============================================================
17
-
18
- from contextlib import asynccontextmanager
19
-
20
- from fastapi import FastAPI
21
- from fastapi.middleware.cors import CORSMiddleware
22
-
23
- from app.models_loader import resources
24
- from app.routes import router
25
-
26
-
27
- @asynccontextmanager
28
- async def lifespan(app: FastAPI):
29
- """يُنفَّذ مرة عند إقلاع الخادم: تحميل النماذج والبيانات مسبقًا."""
30
- print("🔥 تسخين الموارد (تحميل النماذج والـ embeddings)...")
31
- try:
32
- resources.warmup()
33
- print("✅ الموارد جاهزة")
34
- except FileNotFoundError as e:
35
- # لا نوقف الخادم؛ فقط ننبّه المطوّر أن البيانات ناقصة
36
- print(f"⚠️ تحذير: {e}")
37
- yield
38
- print("👋 إيقاف الخادم")
39
-
40
-
41
- app = FastAPI(
42
- title="القاموس العكسي للعربية — API",
43
- description="واجهة برمجية لمقارنة 3 نماذج Embedding في البحث الدلالي العربي",
44
- version="1.0.0",
45
- lifespan=lifespan,
46
- )
47
-
48
- # ------------------------------------------------------------
49
- # 🌐 CORS: نسمح للواجهة (تعمل على منفذ مختلف) باستدعاء الـ API
50
- # ملاحظة: في الإنتاج، حدّد النطاقات المسموحة بدل "*"
51
- # ------------------------------------------------------------
52
- app.add_middleware(
53
- CORSMiddleware,
54
- allow_origins=["*"],
55
- allow_methods=["*"],
56
- allow_headers=["*"],
57
- )
58
-
59
- # ربط نقاط النهاية
60
- app.include_router(router)
61
-
62
-
63
- @app.get("/")
64
- def root():
65
- """نقطة فحص بسيطة للتأكّد أن الخادم يعمل."""
66
- return {"status": "ok", "message": "الخادم يعمل ✅ — التوثيق على /docs"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
reverse-dictionary/backend/requirements.txt CHANGED
@@ -1,16 +1,13 @@
1
  # ============================================================
2
  # 📄 الملف: requirements.txt
3
- # 🎯 الغرض: قائمة مكتبات بايثون اللازمة لتشغيل الـ backend.
4
  # التثبيت: pip install -r requirements.txt
5
  # ============================================================
6
 
7
- fastapi # إطار عمل الـ API
8
- uvicorn[standard] # خادم تشغيل FastAPI
9
- pydantic # التحقّق من البيانات
10
  python-dotenv # قراءة ملف .env
11
  pandas # معالجة البيانات
12
  numpy # العمليات الرقمية
13
  scikit-learn # حساب تشابه الجيب التمام (cosine)
14
- sentence-transformers # نماذج الـ embedding
15
  torch # محرّك التعلّم العميق (يأتي مع sentence-transformers)
16
- streamlit # واجهة Streamlit (frontend/streamlit_app.py)
 
1
  # ============================================================
2
  # 📄 الملف: requirements.txt
3
+ # 🎯 الغرض: قائمة مكتبات بايثون اللازمة لتشغيل المشروع (Streamlit).
4
  # التثبيت: pip install -r requirements.txt
5
  # ============================================================
6
 
7
+ streamlit # الواجهة (frontend/streamlit_app.py)
 
 
8
  python-dotenv # قراءة ملف .env
9
  pandas # معالجة البيانات
10
  numpy # العمليات الرقمية
11
  scikit-learn # حساب تشابه الجيب التمام (cosine)
12
+ sentence-transformers # نماذج الـ embedding + Cross-Encoder (reranker)
13
  torch # محرّك التعلّم العميق (يأتي مع sentence-transformers)
 
reverse-dictionary/frontend/streamlit_app.py CHANGED
@@ -146,11 +146,12 @@ def cached_search(query: str, model_key: str, rerank: bool):
146
  # ============================================================
147
  # 🖥️ الواجهة
148
  # ============================================================
149
- st.markdown("<h2 style='text-align:center;'>🔍 البحث الدلالي</h2>",
150
  unsafe_allow_html=True)
151
  st.markdown(
152
  "<p style='text-align:center; color:#78909C;'>"
153
- "اكتب تعريفًا، وأشّر ☑️ الكلمات المناسبة في كل نموذج، ثم احفظ التقييم."
 
154
  "</p>", unsafe_allow_html=True)
155
 
156
  # زر بحث على اليسار + شريط الإدخال على اليمين
 
146
  # ============================================================
147
  # 🖥️ الواجهة
148
  # ============================================================
149
+ st.markdown("<h2 style='text-align:center;'>🔍 تقييم البحث الدلالي</h2>",
150
  unsafe_allow_html=True)
151
  st.markdown(
152
  "<p style='text-align:center; color:#78909C;'>"
153
+ "ساهِم في تقييم النماذج: اكتب تعريفًا، واطّلع على النتائج المقترحة من كل "
154
+ "نموذج، ثم اختر التقييم الأنسب لكل نموذج."
155
  "</p>", unsafe_allow_html=True)
156
 
157
  # زر بحث على اليسار + شريط الإدخال على اليمين