UT-AI / app.py
Turkiii0's picture
Update app.py
cbc8f8c verified
import os
import re
import pandas as pd
import torch
import gradio as gr
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
# =========================
# 1) إعداد المسارات
# =========================
# لو حاب تنتقل لـ 3B:
# BASE_MODEL = "meta-llama/Llama-3.2-3B-Instruct"
# ADAPTER_REPO = "Turkiii0/UT-AI-3B-LoRA"
# لو تبي تبقى على 1B خله زي ما هو:
BASE_MODEL = "meta-llama/Llama-3.2-3B-Instruct"
ADAPTER_REPO = "Turkiii0/UT-AI-model" # عدّل هنا اسم ريبو اللورا لو غيرته
EXCEL_FILE = "1000 Q.xlsx" # اسم ملف الإكسل
SIM_THRESHOLD = 0.60 # عتبة التشابه لجواب الإكسل
MAX_RAG_ANSWER_LEN = 220 # أقصى طول نسمح فيه لإجابة الإكسل
HF_TOKEN = os.getenv("HF_TOKEN")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# =========================
# 2) تحميل المودل + LoRA
# =========================
print("🔐 HF Token:", "Found" if HF_TOKEN else "Missing")
tokenizer = AutoTokenizer.from_pretrained(
BASE_MODEL,
use_auth_token=HF_TOKEN,
use_fast=True
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
print("🧠 Loading base model...")
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
use_auth_token=HF_TOKEN,
torch_dtype=dtype
)
print("🧩 Loading LoRA adapter...")
model = PeftModel.from_pretrained(
base_model,
ADAPTER_REPO,
use_auth_token=HF_TOKEN
)
model.to(device)
model.eval()
print("✅ Model ready on:", device)
# =========================
# 3) تحميل الأسئلة من الإكسل (RAG)
# =========================
df = pd.read_excel(EXCEL_FILE)
print("🧾 Columns:", list(df.columns))
q_candidates = [c for c in df.columns
if "سؤال" in str(c).lower() or "question" in str(c).lower()]
a_candidates = [c for c in df.columns
if "جواب" in str(c).lower() or "answer" in str(c).lower()]
if q_candidates and a_candidates:
QCOL = q_candidates[0]
ACOL = a_candidates[0]
else:
QCOL = df.columns[0]
ACOL = df.columns[1]
df = df[[QCOL, ACOL]]
df.columns = ["question", "answer"]
df["question"] = df["question"].astype(str).str.strip()
df["answer"] = df["answer"].astype(str).str.strip()
qa_data = df.to_dict(orient="records")
print("📚 Loaded RAG entries:", len(qa_data))
# =========================
# 4) تطبيع السؤال (Normalization)
# =========================
def extract_code(text: str):
"""استخراج كود المقرر مثل CIT1302."""
m = re.search(r"[A-Za-z]{2,4}\s?\d{3,4}", text)
if m:
return m.group(0).replace(" ", "").upper()
return None
def normalize_question(q: str) -> str:
q = q.strip()
match = re.search(r'\b([A-Za-z]{2,4}\s?\d{3,4})\b', q)
course = match.group(1).replace(" ", "") if match else None
if not course:
return q # سؤال ما فيه رمز مادة
lower_q = q.lower()
# سؤال قصير جداً مثل: CIT1302
if lower_q == course.lower():
return f"ماهو مقرر {course}؟"
# متطلبات المادة
if "متطلبات" in lower_q or "متطلب" in lower_q or "prereq" in lower_q:
return f"ماهي متطلبات {course}؟"
# اسم المقرر
if "اسم" in lower_q or "name" in lower_q:
return f"ما اسم مقرر {course}؟"
# عدد ساعات
if "ساع" in lower_q or "hour" in lower_q:
return f"كم عدد ساعات مقرر {course}؟"
# fallback
return f"معلومات عن مقرر {course}"
# =========================
# 5) تشابه ذكي (RAG)
# =========================
AR_STOPWORDS = {
"ما", "هو", "هي", "هل", "عن", "في", "من", "الى", "إلى",
"مادة", "مقرر", "المقرر", "المادة", "ماهي", "ماهو",
"كم", "متطلبات", "متطلب", "متى"
}
def tokenize(text: str):
text = text.lower()
tokens = re.findall(r"[ء-يA-Za-z0-9]+", text)
tokens = [t for t in tokens if t not in AR_STOPWORDS and len(t) > 1]
return set(tokens)
def smart_similarity(user_q: str, candidate_q: str, candidate_a: str = "") -> float:
u_tokens = tokenize(user_q)
q_tokens = tokenize(candidate_q)
a_tokens = tokenize(candidate_a)
if not u_tokens:
return 0.0
inter_q = u_tokens & q_tokens
inter_a = u_tokens & a_tokens
score_q = len(inter_q) / len(u_tokens)
score_a = len(inter_a) / len(u_tokens)
score = score_q * 0.7 + score_a * 0.3
code = extract_code(user_q)
if code:
code_nospace = code.replace(" ", "").lower()
if code_nospace in (candidate_q + " " + candidate_a).replace(" ", "").lower():
score += 0.3
return score
def filter_by_code(code: str, records):
"""نفلتر السطور اللي تحتوي نفس كود المادة قدر الإمكان."""
if not code:
return list(records)
code_nospace = code.replace(" ", "").upper()
out = []
for rec in records:
text = (rec["question"] + " " + rec["answer"]).replace(" ", "").upper()
if code_nospace in text:
out.append(rec)
return out or list(records)
def best_match(user_q: str, records):
best = None
best_sim = 0.0
for rec in records:
sim = smart_similarity(user_q, rec["question"], rec["answer"])
if sim > best_sim:
best = rec
best_sim = sim
return best, best_sim
# =========================
# 6) توليد الجواب من المودل + تنظيف التكرار
# =========================
SYSTEM_PROMPT = (
"أنت مساعد أكاديمي متخصص في جامعة تبوك. "
"أجب فقط بالمعلومة المطلوبة (اسم مقرر، متطلب سابق، عدد ساعات، أو ضابط أكاديمي محدد) "
"بدون شرح إضافي وبدون كلام زائد."
)
def clean_repetition(text: str) -> str:
"""يحاول يشيل التكرار بعد الفواصل العربية."""
parts = [p.strip() for p in text.split("،") if p.strip()]
seen = set()
out = []
for p in parts:
if p not in seen:
out.append(p)
seen.add(p)
return "، ".join(out) if out else text
def generate_from_model(q: str) -> str:
msgs = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": q},
]
prompt = tokenizer.apply_chat_template(
msgs,
add_generation_prompt=True,
tokenize=False
)
inputs = tokenizer(
prompt,
return_tensors="pt",
truncation=True,
padding=True,
max_length=512
).to(device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=80,
do_sample=True,
temperature=0.3,
top_p=0.9,
repetition_penalty=1.35,
no_repeat_ngram_size=4,
eos_token_id=tokenizer.eos_token_id,
pad_token_id=tokenizer.pad_token_id
)
prompt_len = inputs["input_ids"].shape[-1]
out_ids = outputs[0][prompt_len:]
ans = tokenizer.decode(out_ids, skip_special_tokens=True).strip()
ans = " ".join(ans.split())
ans = clean_repetition(ans)
return ans if ans else "لم أجد إجابة واضحة لهذا السؤال."
# =========================
# 7) Hybrid Answer
# =========================
def smart_answer(user_q: str) -> str:
user_q = user_q.strip()
if not user_q:
return "اكتب سؤالك أولاً."
normalized = normalize_question(user_q)
code = extract_code(normalized)
# نختار المرشحين حسب الكود إن وجد
candidates = filter_by_code(code, qa_data)
rec, sim = best_match(normalized, candidates)
# نستخدم جواب الإكسل فقط لو:
# - التشابه عالي
# - وطول الجواب معقول (مو قائمة طويلة)
if rec and sim >= SIM_THRESHOLD and len(rec["answer"]) <= MAX_RAG_ANSWER_LEN:
return rec["answer"].strip()
# غير كذا نخلي المودل يجاوب
return generate_from_model(normalized)
# =========================
# 8) واجهة Gradio
# =========================
def chat_fn(msg, history):
return smart_answer(msg)
chat_ui = gr.ChatInterface(
fn=chat_fn,
title="مرشد التقويم الأكاديمي - جامعة تبوك",
description="اسأل عن: المتطلبات، أسماء المقررات، الساعات، والمعلومات الأكاديمية.",
examples=[
"CIT 1302",
"متطلبات CIT1302",
"اسم CIT 1401",
"كم ساعات CS 322",
"Prerequisite MGT 211"
]
)
if __name__ == "__main__":
chat_ui.launch()