Spaces:
Sleeping
Sleeping
File size: 21,837 Bytes
ada959a c794519 951b2dd 8f44aff e796be6 8f44aff f33a68d ef58159 0834fe1 ef58159 f33a68d 0834fe1 ef58159 2cacd8b ad84a4e e796be6 ad84a4e e796be6 4248f42 e796be6 2cacd8b ed72a14 2cacd8b 0834fe1 2cacd8b f33a68d 0834fe1 2cacd8b c6eb002 2cacd8b c6eb002 ad84a4e d1892ca ad84a4e e2e06b0 ad84a4e c6eb002 f33a68d ad84a4e f33a68d ad84a4e f33a68d ad84a4e c6eb002 ad84a4e c6eb002 c794519 e796be6 c794519 e796be6 c794519 e796be6 c794519 e796be6 c794519 e796be6 c794519 e796be6 c794519 6a8062d c794519 e796be6 c794519 c6eb002 30e54a3 0834fe1 30e54a3 0834fe1 30e54a3 0834fe1 ad84a4e d1892ca ad84a4e d1892ca ad84a4e 2cacd8b ef58159 2cacd8b 30e54a3 2cacd8b 8f44aff ada959a ef58159 8f44aff 30e54a3 ef58159 c794519 ef58159 c794519 ef58159 30e54a3 c6eb002 c794519 8f44aff ada959a 2cacd8b ada959a 2cacd8b ada959a 2cacd8b ada959a 8f44aff ef58159 0834fe1 544ee8f 8f44aff ef58159 8f44aff ef58159 c794519 2cacd8b ef58159 c794519 2cacd8b ef58159 2cacd8b c794519 8f44aff ef58159 8f44aff 2cacd8b c794519 8f44aff ada959a c794519 8f44aff ad84a4e 8f44aff c794519 8f44aff c794519 8f44aff ada959a 8f44aff d449a13 ada959a 2cacd8b 8f44aff ada959a 6a8062d ada959a 8f44aff ef58159 2cacd8b ada959a 2cacd8b 8f44aff 825c8ee 8f44aff ada959a 8f44aff 2cacd8b 30e54a3 8f44aff 2cacd8b 30e54a3 8f44aff ada959a 8f44aff 2a8cf74 0834fe1 c794519 ef58159 9da4b8a c794519 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 | import os
from threading import Thread
import gradio as gr
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TextIteratorStreamer,
StoppingCriteria,
StoppingCriteriaList,
)
from openai import OpenAI
# ==========================================
# النماذج المتاحة (مساران):
# 1) Qwen3.5-2B → عبر Transformers، يدعم وضع التفكير (thinking) القابل للتبديل.
# 2) GPT-5 → عبر الـ OpenAI API.
# النموذج المحلي يُحمّل كسولاً عند أول استخدام لتوفير الذاكرة عند الإقلاع.
# ==========================================
QWEN_MODEL = "Qwen/Qwen3.5-2B"
GPT_LABEL = "GPT-5 (OpenAI API)"
# حدود الرموز (Tokens):
# - رسالة المستخدم الواحدة: 512 رمزاً كحد أقصى (يُقصّ الزائد).
# - كامل سياق المحادثة المُرسل للنموذج: 4096 رمزاً (نُبقي الأحدث).
MAX_MSG_TOKENS = 1024
MAX_CHAT_TOKENS = 4096
# ميزانيتان منفصلتان للتوليد في وضع التفكير:
# - THINK_BUDGET : أقصى عدد رموز يُسمح به لكواليس التفكير. إن نفدت دون أن يُغلق
# النموذج التفكير (</think>) نُغلقه قسراً وننتقل لتوليد الجواب.
# - ANSWER_BUDGET: ميزانية مستقلة للجواب، فلا يبتلع التفكيرُ حصة الجواب أبداً.
# بهذا نضمن وصول جواب فعلي حتى لو أطال النموذج في التفكير.
THINK_BUDGET =512
ANSWER_BUDGET =512
THINK_END_ID = 248069
_qwen_models = {} # سجلّ نماذج Qwen المحمّلة عبر Transformers
# ------------------------------------------
# تحميل Qwen عبر Transformers (bfloat16 على الـ CPU لتقليل ذروة الذاكرة)
# ------------------------------------------
def get_qwen_model(model_id):
if model_id not in _qwen_models:
print(f"⏳ جاري تحميل نموذج {model_id} ...")
tok = AutoTokenizer.from_pretrained(model_id)
if torch.cuda.is_available():
# على الـ GPU: تحميل عادي بالدقة التلقائية
mdl = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="auto", device_map="auto")
else:
# على الـ CPU: نحمّل بدقة bfloat16 بدون تكميم int8.
# السبب: تكميم float32→int8 يُحدث ذروة ذاكرة ضخمة (~10-11GB لنموذج 2B) تسبب OOM
# على الـ Space المجاني. تحميل bfloat16 يُبقي الذروة عند ~نصف ذلك فتتّسع النماذج الأكبر،
# مقابل توليد أبطأ قليلاً على الـ CPU (لا تسريع int8).
print("🔻 تحميل بدقة bfloat16 على CPU لتقليل ذروة استهلاك الذاكرة...")
mdl = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, low_cpu_mem_usage=True)
_qwen_models[model_id] = (tok, mdl)
return _qwen_models[model_id]
# ==========================================
# توليد Qwen عبر Transformers مع دعم وضع التفكير القابل للتبديل
# Qwen3 يوصي: وضع التفكير temp=0.6/top_p=0.95 — الوضع العادي temp=0.7/top_p=0.8 — وtop_k=20 دائماً
# ==========================================
def _truncate_text_tokens(tok, text, max_tokens):
"""يقصّ النص إلى max_tokens رمزاً كحد أقصى (نُبقي البداية).
نتجاهل ما ليس نصاً (None/غير str) تماماً كما يفعل قالب المحادثة، تفادياً لخطأ المُرمِّز."""
if not isinstance(text, str) or not text:
return text
ids = tok(text, add_special_tokens=False).input_ids
if len(ids) <= max_tokens:
return text
return tok.decode(ids[:max_tokens], skip_special_tokens=True)
def qwen_generate(messages, enable_thinking, tok, mdl, max_new_tokens=1024):
temperature = 0.6 if enable_thinking else 0.7
top_p = 0.95 if enable_thinking else 0.8
device = next(mdl.parameters()).device
# حدّ رسالة المستخدم: نقصّ كل رسالة مستخدم إلى MAX_MSG_TOKENS رمزاً قبل بناء البرومبت
messages = [dict(m) for m in messages]
for m in messages:
if m["role"] == "user":
m["content"] = _truncate_text_tokens(tok, m["content"], MAX_MSG_TOKENS)
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=enable_thinking)
model_inputs = tok([text], return_tensors="pt").to(device)
# حدّ سياق المحادثة: إن تجاوز الإدخال MAX_CHAT_TOKENS نُبقي آخر (الأحدث) رموزه فقط
input_ids = model_inputs.input_ids
attention_mask = model_inputs.attention_mask
if input_ids.shape[-1] > MAX_CHAT_TOKENS:
input_ids = input_ids[:, -MAX_CHAT_TOKENS:]
attention_mask = attention_mask[:, -MAX_CHAT_TOKENS:]
generated_ids = mdl.generate(
input_ids=input_ids,
attention_mask=attention_mask,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=temperature,
top_p=top_p,
top_k=20,
)
return generated_ids[0][len(input_ids[0]):].tolist()
# ==========================================
# معيار إيقاف: نوقف التوليد فور إنتاج رمز نهاية التفكير </think>
# ==========================================
class _StopOnToken(StoppingCriteria):
def __init__(self, token_id):
self.token_id = token_id
def __call__(self, input_ids, scores, **kwargs):
return input_ids[0, -1].item() == self.token_id
# ==========================================
# صياغة العرض: نغلّف التفكير في صندوق قابل للطي ثم نُلحق الجواب
# ==========================================
def _compose_output(badge, thinking, answer):
if thinking:
return f"{badge}<details><summary>🧠 كواليس تفكير النموذج</summary>\n\n{thinking}\n\n</details>\n\n{answer}"
return f"{badge}{answer}"
# دالة مساعدة: تُشغّل التوليد على خيط منفصل وتبثّ النص، وتُعيد تسلسل الرموز الكامل
def _run_stream(mdl, tok, gen_kwargs):
streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True)
gen_kwargs = dict(gen_kwargs, streamer=streamer)
box = {}
def _run():
box["out"] = mdl.generate(**gen_kwargs)
thread = Thread(target=_run, daemon=True)
thread.start()
for new_text in streamer:
yield ("chunk", new_text)
thread.join()
yield ("done", box.get("out"))
# ==========================================
# نسخة بثّية من توليد Qwen بميزانيتين منفصلتين (تفكير/جواب).
# في وضع التفكير: مرحلة 1 تولّد التفكير حتى </think> أو نفاد THINK_BUDGET،
# ثم نُغلق التفكير قسراً ونشغّل مرحلة 2 لتوليد الجواب بميزانية ANSWER_BUDGET
# مستقلة — فلا يبتلع التفكيرُ حصةَ الجواب أبداً.
# ==========================================
def qwen_generate_stream(messages, enable_thinking, tok, mdl, badge):
temperature = 0.6 if enable_thinking else 0.7
top_p = 0.95 if enable_thinking else 0.8
device = next(mdl.parameters()).device
# حدّ رسالة المستخدم: نقصّ كل رسالة مستخدم إلى MAX_MSG_TOKENS رمزاً قبل بناء البرومبت
messages = [dict(m) for m in messages]
for m in messages:
if m["role"] == "user":
m["content"] = _truncate_text_tokens(tok, m["content"], MAX_MSG_TOKENS)
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=enable_thinking)
model_inputs = tok([text], return_tensors="pt").to(device)
# حدّ سياق المحادثة: إن تجاوز الإدخال MAX_CHAT_TOKENS نُبقي آخر (الأحدث) رموزه فقط
input_ids = model_inputs.input_ids
attention_mask = model_inputs.attention_mask
if input_ids.shape[-1] > MAX_CHAT_TOKENS:
input_ids = input_ids[:, -MAX_CHAT_TOKENS:]
attention_mask = attention_mask[:, -MAX_CHAT_TOKENS:]
sampling = dict(do_sample=True, temperature=temperature, top_p=top_p, top_k=20,repetition_penalty=1.05)
# ---- الوضع العادي (بلا تفكير): مرحلة واحدة لتوليد الجواب ----
if not enable_thinking:
raw = ""
for kind, payload in _run_stream(mdl, tok, dict(
input_ids=input_ids, attention_mask=attention_mask,
max_new_tokens=ANSWER_BUDGET, **sampling,
)):
if kind == "chunk":
raw += payload
yield _compose_output(badge, "", raw.strip("\n"))
yield _compose_output(badge, "", raw.strip("\n") or "⚠️ لم يصل أي جواب.")
return
# ---- وضع التفكير: مرحلة 1 — توليد التفكير حتى </think> أو نفاد الميزانية ----
think_out = None
think_raw = ""
for kind, payload in _run_stream(mdl, tok, dict(
input_ids=input_ids, attention_mask=attention_mask,
max_new_tokens=THINK_BUDGET,
stopping_criteria=StoppingCriteriaList([_StopOnToken(THINK_END_ID)]),
**sampling,
)):
if kind == "chunk":
think_raw += payload
# نعرض التفكير مباشرة داخل الصندوق أثناء توليده (الجواب لم يبدأ بعد)
thinking_live = think_raw.split("</think>")[0].replace("<think>", "").strip("\n")
yield _compose_output(badge, thinking_live, "")
else:
think_out = payload
thinking_text = think_raw.split("</think>")[0].replace("<think>", "").strip("\n")
# ---- المرحلة 2: نضمن إغلاق التفكير ثم نولّد الجواب بميزانية مستقلة ----
gen_ids = think_out
if THINK_END_ID not in gen_ids[0].tolist():
# نفدت ميزانية التفكير دون إغلاق → نُغلقه قسراً بإلحاق </think>
close = tok("</think>\n\n", add_special_tokens=False, return_tensors="pt").input_ids.to(device)
gen_ids = torch.cat([gen_ids, close], dim=-1)
attn2 = torch.ones_like(gen_ids)
answer_raw = ""
for kind, payload in _run_stream(mdl, tok, dict(
input_ids=gen_ids, attention_mask=attn2,
max_new_tokens=ANSWER_BUDGET, **sampling,
)):
if kind == "chunk":
answer_raw += payload
yield _compose_output(badge, thinking_text, answer_raw.strip("\n"))
answer = answer_raw.strip("\n") or "⚠️ لم يكتمل الجواب ضمن حدّ التوليد. جرّب رفع ANSWER_BUDGET أو إعادة المحاولة."
yield _compose_output(badge, thinking_text, answer)
# ==========================================
# تحميل مسبق للنموذج عند بدء التشغيل بدل التحميل الكسول
# نحمّل Qwen مقدّماً حتى لا يتأخر أول ردّ للمستخدم.
# (تبقى دالة get_qwen_model كما هي فتجد النموذج محمّلاً في سجلّه)
# ==========================================
def preload_models():
print("🚀 تحميل مسبق للنموذج (Qwen)...")
get_qwen_model(QWEN_MODEL)
print("✅ اكتمل التحميل المسبق للنموذج.")
# ==========================================
# حدود الرموز لمسار OpenAI: نعدّ الرموز بـ tiktoken، ومع بديل تقريبي إن لم تتوفّر
# ==========================================
try:
import tiktoken
_OAI_ENC = tiktoken.get_encoding("o200k_base") # ترميز نماذج GPT الحديثة
except Exception:
_OAI_ENC = None
def _oai_truncate(text, max_tokens):
"""يقصّ نصاً إلى max_tokens رمزاً (بـ tiktoken، أو تقدير ~4 أحرف/رمز كبديل)."""
if not isinstance(text, str) or not text:
return text
if _OAI_ENC is None:
return text if len(text) <= max_tokens * 4 else text[: max_tokens * 4]
ids = _OAI_ENC.encode(text)
if len(ids) <= max_tokens:
return text
return _OAI_ENC.decode(ids[:max_tokens])
def _oai_count(text):
if not isinstance(text, str) or not text:
return 0
return len(_OAI_ENC.encode(text)) if _OAI_ENC is not None else max(1, len(text) // 4)
def _oai_fit_messages(messages, max_tokens):
"""يقصّ كل رسالة مستخدم إلى MAX_MSG_TOKENS، ويُبقي المحادثة ضمن max_tokens
عبر إسقاط أقدم الرسائل مع الحفاظ على رسالة النظام."""
msgs = [dict(m) for m in messages]
for m in msgs:
if m["role"] == "user":
m["content"] = _oai_truncate(m["content"], MAX_MSG_TOKENS)
system = msgs[0] if msgs and msgs[0]["role"] == "system" else None
body = msgs[1:] if system else msgs
total = lambda: (_oai_count(system["content"]) if system else 0) + sum(_oai_count(m["content"]) for m in body)
while total() > max_tokens and len(body) > 1:
body.pop(0) # نُسقط الأقدم (نُبقي الأحدث)
return ([system] if system else []) + body
# ==========================================
# تنظيف رسائل المساعد القديمة من الشارات/كواليس التفكير قبل تمريرها للنموذج
# ==========================================
def _strip_assistant_extras(content):
if not isinstance(content, str):
return ""
if "</details>" in content: # إزالة صندوق التفكير (Qwen)
content = content.split("</details>")[-1].strip()
if content.startswith("🟢") and "\n\n" in content: # إزالة شارة المسار
content = content.split("\n\n", 1)[-1].strip()
return content
# ==========================================
# الدالة الأساسية لمعالجة المحادثة بنظام الـ Messages الحديث
# (غلاف يلتقط أي خطأ ويعرضه نصاً في الشات بدل شارة Gradio العامة "Error")
# ==========================================
def chat_engine(user_input, chat_history, model_choice, enable_thinking, api_key, system_prompt):
try:
yield from _chat_engine_impl(user_input, chat_history, model_choice, enable_thinking, api_key, system_prompt)
except Exception as e:
import traceback
traceback.print_exc()
if chat_history is None:
chat_history = []
chat_history.append({"role": "assistant", "content": f"❌ خطأ عام في المعالجة:\n\n`{type(e).__name__}: {str(e)}`"})
yield chat_history, ""
def _chat_engine_impl(user_input, chat_history, model_choice, enable_thinking, api_key, system_prompt):
if not user_input.strip():
yield chat_history, ""
return
# 1. تهيئة مصفوفة الرسائل ببرومبت النظام (System Prompt)
messages = [{"role": "system", "content": system_prompt}]
# 2. تفكيك التاريخ القديم بصيغة الـ Dicts الجديدة
for msg in chat_history:
role = msg["role"]
content = msg["content"]
if role == "assistant":
content = _strip_assistant_extras(content)
messages.append({"role": role, "content": content})
# 3. إضافة رسالة المستخدم الجديدة الحالية
messages.append({"role": "user", "content": user_input})
# 4. تقليم التاريخ: نُبقي آخر تبادل واحد فقط (سؤال+جواب) حتى لا يتضخّم السياق
MAX_HISTORY_MSGS = 4
if len(messages) > MAX_HISTORY_MSGS :
messages = [messages[0]] + messages[-(MAX_HISTORY_MSGS + 1):]
# ------------------------------------------
# المسار الأول: نموذج Qwen3 المحلي
# ------------------------------------------
if model_choice == QWEN_MODEL:
badge = "🟢 *ردّ مباشر من Qwen*\n\n"
# نضيف رسالة المستخدم ورسالة مساعد فارغة نُحدّثها تدريجياً مع تدفّق الرموز
chat_history.append({"role": "user", "content": user_input})
chat_history.append({"role": "assistant", "content": badge})
try:
tok, mdl = get_qwen_model(model_choice)
# البثّ: كل دفعة نُحدّث آخر رسالة مساعد ونُنتج الحالة الجديدة للعرض الحيّ
for final_output in qwen_generate_stream(messages, enable_thinking, tok, mdl, badge):
chat_history[-1]["content"] = final_output
yield chat_history, ""
except Exception as e:
# نُظهر الخطأ الفعلي بدل رسالة Gradio العامة حتى يسهل تشخيصه
import traceback
traceback.print_exc()
chat_history[-1]["content"] = f"❌ خطأ أثناء تشغيل النموذج المحلي ({model_choice}):\n\n`{type(e).__name__}: {str(e)}`"
yield chat_history, ""
return
# ------------------------------------------
# المسار الثاني: GPT-5 عبر الـ API
# ------------------------------------------
elif model_choice == GPT_LABEL:
chat_history.append({"role": "user", "content": user_input})
if not api_key.strip():
chat_history.append({"role": "assistant", "content": "⚠️ يرجى إدخال الـ API Key أولاً."})
yield chat_history, ""
return
# رسالة مساعد فارغة نُراكم عليها دفعات البثّ
chat_history.append({"role": "assistant", "content": ""})
try:
# نطبّق حدّي الرموز: 512 لكل رسالة مستخدم و4096 لكامل السياق
messages = _oai_fit_messages(messages, MAX_CHAT_TOKENS)
client = OpenAI(api_key=api_key)
# stream=True: نستقبل الردّ على شكل دفعات ونعرضها فور وصولها
stream = client.chat.completions.create(
model="gpt-5", messages=messages, max_tokens=1024, stream=True
)
acc = ""
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
if delta:
acc += delta
chat_history[-1]["content"] = acc
yield chat_history, ""
if not acc:
chat_history[-1]["content"] = "⚠️ لم يصل أي محتوى من الـ API."
yield chat_history, ""
except Exception as e:
chat_history[-1]["content"] = f"❌ خطأ: {str(e)}"
yield chat_history, ""
return
# ==========================================
# بناء واجهة المستخدم (Gradio UI Layout)
# ==========================================
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="slate")) as demo:
gr.Markdown("# 🤖 منصة المحادثة الذكية متعددة النماذج")
with gr.Row():
with gr.Column(scale=1, min_width=280):
gr.Markdown("### ⚙️ إعدادات النظام")
system_prompt_input = gr.Textbox(
label="توجيهات النظام (System Prompt)",
value="you are a funny smart assistant. you anwer briefly and accurately and in standard Arabic",
lines=4,
placeholder="اكتب تعليمات الموديل هنا..."
)
model_dropdown = gr.Dropdown(choices=[QWEN_MODEL, GPT_LABEL], value=QWEN_MODEL, label="اختر النموذج")
thinking_checkbox = gr.Checkbox(label="🧠 تفعيل وضع التفكير (Qwen فقط)", value=True)
api_key_input = gr.Textbox(label="OpenAI API Key", type="password")
clear_button = gr.Button("🧹 مسح الذاكرة")
with gr.Column(scale=4):
chatbot = gr.Chatbot(label="شاشة الشات", height=550)
with gr.Row():
user_msg = gr.Textbox(placeholder="اكتب سؤالك هنا واضغط Enter...", scale=4)
submit_button = gr.Button("إرسال 🚀", variant="primary", scale=1)
submit_event = submit_button.click(
chat_engine,
inputs=[user_msg, chatbot, model_dropdown, thinking_checkbox, api_key_input, system_prompt_input],
outputs=[chatbot, user_msg]
)
user_msg.submit(
chat_engine,
inputs=[user_msg, chatbot, model_dropdown, thinking_checkbox, api_key_input, system_prompt_input],
outputs=[chatbot, user_msg]
)
clear_button.click(fn=lambda: [], outputs=chatbot, queue=False)
if __name__ == "__main__":
# تحميل النماذج مقدّماً قبل رفع الواجهة حتى يكون أول ردّ سريعاً.
preload_models()
# نفعّل الطابور (queue) حتى يعمل البثّ التدريجي للدوال المُولّدة (generators)
demo.queue()
# على Colab نحتاج share=True لإنشاء رابط عام؛ على Hugging Face Space يجب ألا نفعّله.
on_hf_space = os.environ.get("SPACE_ID") is not None
demo.launch(share=not on_hf_space)
|