Zero-Shot Classification
Transformers
Safetensors
Arabic
bert
feature-extraction
arabic
prompt-routing
router
encoder
tiny-model
Instructions to use oddadmix/Nawah-Router-BERT-6M-v2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use oddadmix/Nawah-Router-BERT-6M-v2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("zero-shot-classification", model="oddadmix/Nawah-Router-BERT-6M-v2")# Load model directly from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("oddadmix/Nawah-Router-BERT-6M-v2") model = AutoModel.from_pretrained("oddadmix/Nawah-Router-BERT-6M-v2", device_map="auto") - Notebooks
- Google Colab
- Kaggle
add router_axes.py
Browse files- router_axes.py +460 -0
router_axes.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Arabic prompt-routing corpus v2 — many routing axes, and lane sets that mix them.
|
| 3 |
+
|
| 4 |
+
v1 (router_common.py) generated one axis only: topical intent inside a business vertical, 18
|
| 5 |
+
domains. It reached 0.9199 on unseen lane sets but stalled at 0.6665 on unseen domains, and the
|
| 6 |
+
reference demo shows why that is the wrong shape rather than merely too small. LiquidAI's shipped
|
| 7 |
+
presets mix axes inside a single lane set:
|
| 8 |
+
|
| 9 |
+
Simple function call · Simple tool use · Complex multi-step agentic task ·
|
| 10 |
+
Quick factual question · Casual conversation · Creative writing · Translation ·
|
| 11 |
+
Needs a bigger model
|
| 12 |
+
|
| 13 |
+
That is complexity, task type, tool use and an escalation lane together. Every v1 lane set was
|
| 14 |
+
single-axis, so the model never saw categories that differ along different dimensions at once.
|
| 15 |
+
|
| 16 |
+
Three generation modes here:
|
| 17 |
+
|
| 18 |
+
topical - v1's mode, widened from 18 to 45 verticals. The model invents the lanes.
|
| 19 |
+
axis - lanes drawn from one of ten taxonomies (task type, complexity, safety, retrieval,
|
| 20 |
+
tools, subject, subdomain, language, urgency, tone), each with several paraphrased
|
| 21 |
+
wordings so the model learns the concept and not the string.
|
| 22 |
+
mixed - lanes sampled across two or three axes, as the reference does. The largest share.
|
| 23 |
+
|
| 24 |
+
`router_common.py` is deliberately untouched: it defines build_task for task ids 100000-116063 and
|
| 25 |
+
editing it would silently redraw prompts for the 51,392 rows already generated. This module owns a
|
| 26 |
+
disjoint range (START_TASK=1000000) and its output is merged with v1's at build time.
|
| 27 |
+
|
| 28 |
+
Labels are still established by construction and confirmed by the independent back-classification
|
| 29 |
+
in verify_router.py, which is axis-agnostic and needs no change.
|
| 30 |
+
"""
|
| 31 |
+
import random
|
| 32 |
+
import re
|
| 33 |
+
import unicodedata
|
| 34 |
+
|
| 35 |
+
ITEMS_PER_TASK = 4
|
| 36 |
+
|
| 37 |
+
C, Q, A, E = "### فئات", "### نص", "### الفئة", "### نهاية"
|
| 38 |
+
|
| 39 |
+
# Varied on purpose. v1 used one fixed string, which teaches the token rather than the concept;
|
| 40 |
+
# the reference ships a catch-all in most presets under several different names.
|
| 41 |
+
OTHER_LANES = ["أخرى / لا ينطبق", "غير ذلك", "لا ينطبق أي مما سبق", "أخرى",
|
| 42 |
+
"خارج التصنيفات المذكورة"]
|
| 43 |
+
|
| 44 |
+
# ---------------------------------------------------------------- axis taxonomies
|
| 45 |
+
# Each lane carries several wordings; build_task picks one per task so the corpus never ties a
|
| 46 |
+
# concept to a single surface form. Wordings deliberately avoid a shared give-away token
|
| 47 |
+
# (e.g. not every complexity lane contains بسيط/معقد).
|
| 48 |
+
|
| 49 |
+
AXES = {
|
| 50 |
+
"task_type": {
|
| 51 |
+
"hint": "مستخدم يطلب من مساعد ذكي مهمة",
|
| 52 |
+
"lanes": [
|
| 53 |
+
["ترجمة", "نقل نص من لغة إلى أخرى", "طلب ترجمة"],
|
| 54 |
+
["برمجة", "كتابة أو إصلاح كود", "مهمة برمجية"],
|
| 55 |
+
["تلخيص", "اختصار نص طويل", "طلب ملخّص"],
|
| 56 |
+
["كتابة إبداعية", "تأليف قصة أو نص أدبي", "صياغة محتوى إبداعي"],
|
| 57 |
+
["رياضيات", "حساب أو مسألة رياضية", "عملية حسابية"],
|
| 58 |
+
["استخراج بيانات", "انتزاع حقول من نص", "تحويل نص إلى بيانات منظمة"],
|
| 59 |
+
["سؤال معلومات", "طلب معلومة مباشرة", "سؤال معرفي عام"],
|
| 60 |
+
["محادثة", "دردشة غير مهمّية", "كلام عابر"],
|
| 61 |
+
["إعادة صياغة", "تحسين صياغة نص قائم", "تحرير وتنقيح"],
|
| 62 |
+
["تصنيف", "تحديد فئة نص", "فرز محتوى"],
|
| 63 |
+
],
|
| 64 |
+
},
|
| 65 |
+
"complexity": {
|
| 66 |
+
"hint": "مستخدم يكتب طلبًا يتفاوت في صعوبته",
|
| 67 |
+
"lanes": [
|
| 68 |
+
["طلب بسيط ومباشر", "يُنجز بخطوة واحدة", "مهمة سهلة لا تحتاج تفكيرًا"],
|
| 69 |
+
["طلب متوسط", "يحتاج خطوتين أو ثلاثًا", "مهمة معتدلة"],
|
| 70 |
+
["طلب معقّد متعدد الخطوات", "يحتاج تخطيطًا وتسلسلًا طويلًا", "مهمة مركّبة"],
|
| 71 |
+
["يحتاج نموذجًا أكبر", "يفوق قدرة نموذج صغير", "يُحال إلى نموذج أقوى"],
|
| 72 |
+
["يحتاج استدلالًا خطوة بخطوة", "يتطلب سلسلة تفكير", "حل يحتاج برهنة"],
|
| 73 |
+
],
|
| 74 |
+
},
|
| 75 |
+
"safety": {
|
| 76 |
+
"hint": "رسالة واردة تحتاج فرزًا من ناحية السلامة",
|
| 77 |
+
"lanes": [
|
| 78 |
+
["محتوى آمن", "لا مشكلة فيه", "مقبول للنشر"],
|
| 79 |
+
["رسائل مزعجة", "إعلانات متكررة غير مرغوبة", "سبام تجاري"],
|
| 80 |
+
["يحتوي بيانات شخصية", "فيه معلومات تعريف حساسة", "يكشف خصوصية أحدهم"],
|
| 81 |
+
["لغة مسيئة", "ألفاظ جارحة", "إهانة أو تنمّر"],
|
| 82 |
+
],
|
| 83 |
+
},
|
| 84 |
+
"retrieval": {
|
| 85 |
+
"hint": "سؤال قد يحتاج مصدرًا خارجيًا أو لا",
|
| 86 |
+
"lanes": [
|
| 87 |
+
["يُجاب من المعرفة العامة", "لا يحتاج بحثًا", "إجابة مباشرة بلا مصادر"],
|
| 88 |
+
["يحتاج بحثًا واحدًا", "يكفيه مصدر واحد", "استعلام واحد يكفي"],
|
| 89 |
+
["يحتاج بحثًا متعدد الخطوات", "يجمع بين أكثر من مصدر", "يتطلب تتبع عدة معلومات"],
|
| 90 |
+
["يحتاج معلومة لحظية", "يعتمد على بيانات متغيرة الآن", "يحتاج تحديثًا آنيًا"],
|
| 91 |
+
],
|
| 92 |
+
},
|
| 93 |
+
"tools": {
|
| 94 |
+
"hint": "طلب قد يستدعي أداة معينة",
|
| 95 |
+
"lanes": [
|
| 96 |
+
["بحث في الويب", "استعلام محرك بحث", "تصفح الإنترنت"],
|
| 97 |
+
["حاسبة", "إجراء عملية حسابية", "أداة حساب"],
|
| 98 |
+
["تقويم ومواعيد", "جدولة موعد", "إدارة الأجندة"],
|
| 99 |
+
["بريد ورسائل", "إرسال رسالة", "التعامل مع البريد"],
|
| 100 |
+
["قاعدة بيانات", "استعلام من سجلات", "جلب صف من جدول"],
|
| 101 |
+
["لا يحتاج أداة", "يُجاب بلا استدعاء خارجي", "بدون أدوات"],
|
| 102 |
+
["تشغيل كود", "تنفيذ سكربت", "مفسّر برمجي"],
|
| 103 |
+
],
|
| 104 |
+
},
|
| 105 |
+
"subject": {
|
| 106 |
+
"hint": "سؤال أكاديمي في تخصص ما",
|
| 107 |
+
"lanes": [
|
| 108 |
+
["أحياء", "علوم الحياة", "بيولوجيا"],
|
| 109 |
+
["قانون", "مسائل قانونية", "تشريع وأنظمة"],
|
| 110 |
+
["اقتصاد", "مسائل اقتصادية", "اقتصاد وأسواق"],
|
| 111 |
+
["هندسة", "مسائل هندسية", "تصميم وتنفيذ هندسي"],
|
| 112 |
+
["طب وصحة", "مسائل طبية", "صحة وعلاج"],
|
| 113 |
+
["تاريخ", "أحداث ووقائع تاريخية", "ماضٍ وسير"],
|
| 114 |
+
["فلسفة", "مسائل فلسفية", "فكر ومنطق"],
|
| 115 |
+
["فيزياء", "مسائل فيزيائية", "قوانين الطبيعة"],
|
| 116 |
+
["حاسوب", "علوم الحاسب", "تقنية معلومات"],
|
| 117 |
+
["علم نفس", "مسائل نفسية", "سلوك وإدراك"],
|
| 118 |
+
],
|
| 119 |
+
},
|
| 120 |
+
"language": {
|
| 121 |
+
"hint": "رسالة مكتوبة بلهجة أو لغة ما",
|
| 122 |
+
"lanes": [
|
| 123 |
+
["عربية فصحى", "لغة عربية رسمية", "فصحى معاصرة"],
|
| 124 |
+
["لهجة مصرية", "عامية مصر", "مصري"],
|
| 125 |
+
["لهجة خليجية", "عامية الخليج", "خليجي"],
|
| 126 |
+
["لهجة شامية", "عامية بلاد الشام", "شامي"],
|
| 127 |
+
["لهجة مغاربية", "عامية المغرب العربي", "مغاربي"],
|
| 128 |
+
["إنجليزية", "نص بالإنجليزية", "لغة إنجليزية"],
|
| 129 |
+
["عربي مختلط بالإنجليزية", "كتابة مزدوجة اللغة", "عربيزي أو تبديل لغوي"],
|
| 130 |
+
],
|
| 131 |
+
},
|
| 132 |
+
"urgency": {
|
| 133 |
+
"hint": "رسالة تتفاوت في إلحاحها",
|
| 134 |
+
"lanes": [
|
| 135 |
+
["عاجل", "يحتاج تدخلًا فوريًا", "أولوية قصوى"],
|
| 136 |
+
["أولوية عادية", "يمكن معالجته ضمن الدور", "غير مستعجل"],
|
| 137 |
+
["أولوية منخفضة", "يمكن تأجيله", "لا يستدعي استعجالًا"],
|
| 138 |
+
],
|
| 139 |
+
},
|
| 140 |
+
"tone": {
|
| 141 |
+
"hint": "رسالة من عميل بنبرة معينة",
|
| 142 |
+
"lanes": [
|
| 143 |
+
["شكوى غاضبة", "امتعاض شديد", "نبرة تصعيد"],
|
| 144 |
+
["استفسار محايد", "سؤال هادئ", "نبرة عادية"],
|
| 145 |
+
["شكر وثناء", "رسالة امتنان", "إطراء على الخدمة"],
|
| 146 |
+
["طلب اعتذار أو تعويض", "مطالبة بحق", "طلب جبر ضرر"],
|
| 147 |
+
],
|
| 148 |
+
},
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
# Mixed lane sets use COMPOUND lanes, not a union of two axes' lanes.
|
| 152 |
+
#
|
| 153 |
+
# The first attempt sampled lanes from two axes at once and produced ill-posed rows: an insulting
|
| 154 |
+
# message written in Gulf dialect belongs to both 'لهجة خليجية' and 'إهانة أو تنمر', and the task
|
| 155 |
+
# is single-label. Constraining to "compatible" axes did not fix it either - an angry complaint is
|
| 156 |
+
# also urgent. Any two independent axes overlap by construction.
|
| 157 |
+
#
|
| 158 |
+
# The reference avoids this because its lanes are already compound: "Simple function call" fuses
|
| 159 |
+
# complexity with tool use, "Complex multi-step agentic task" fuses complexity with task type.
|
| 160 |
+
# Each lane names a point in the joint space, so the set partitions rather than overlaps. These
|
| 161 |
+
# pools mirror that, one per facet-family; a lane set is drawn from a single pool.
|
| 162 |
+
COMPOUND_POOLS = {
|
| 163 |
+
"assistant": (
|
| 164 |
+
"مستخدم يطلب من مساعد ذكي مهمة",
|
| 165 |
+
["استدعاء دالة بسيط", "استخدام أداة بسيط", "مهمة وكيلة معقدة متعددة الخطوات",
|
| 166 |
+
"استدلال بسيط", "استدلال معقد يحتاج خطوات", "مهمة برمجية بسيطة",
|
| 167 |
+
"مهمة برمجية معقدة", "إصلاح خطأ برمجي صغير", "إعادة هيكلة كود واسعة",
|
| 168 |
+
"مراجعة كود", "كتابة اختبارات", "شرح كود قائم",
|
| 169 |
+
"مسألة حسابية بسيطة", "مسألة رياضية معقدة", "ترجمة", "تلخيص",
|
| 170 |
+
"كتابة إبداعية", "إعادة صياغة نص", "بحث عن معلومة مباشرة",
|
| 171 |
+
"سؤال سريع تُعرف إجابته", "محادثة عابرة", "يحتاج نموذجًا أكبر",
|
| 172 |
+
"يحتاج بحثًا في الويب", "يحتاج تنفيذ كود", "استعلام قاعدة بيانات",
|
| 173 |
+
"تحليل بيانات", "إنشاء رسم بياني", "استخراج حقول من نص",
|
| 174 |
+
"جدولة موعد في التقويم", "صياغة رسالة بريد"],
|
| 175 |
+
),
|
| 176 |
+
"triage": (
|
| 177 |
+
"رسالة واردة إلى فريق خدمة العملاء",
|
| 178 |
+
["شكوى غاضبة تحتاج تصعيدًا فوريًا", "استفسار عادي غير مستعجل",
|
| 179 |
+
"بلاغ عطل حرج يوقف الخدمة", "بلاغ عطل بسيط يمكن تأجيله",
|
| 180 |
+
"طلب استرجاع مبلغ", "طلب إلغاء اشتراك", "رسالة شكر وثناء",
|
| 181 |
+
"رسالة مزعجة إعلانية", "إساءة لفظية تستوجب حظرًا",
|
| 182 |
+
"طلب حذف بيانات شخصية", "استفسار عن الفاتورة", "طلب ميزة جديدة",
|
| 183 |
+
"مشكلة في الدخول إلى الحساب", "سؤال عام عن الخدمة"],
|
| 184 |
+
),
|
| 185 |
+
"content": (
|
| 186 |
+
"نص يُراد تصنيف موضوعه ومستواه",
|
| 187 |
+
["خبر سياسي", "خبر رياضي", "خبر اقتصادي", "تقرير علمي مبسّط",
|
| 188 |
+
"مقال رأي", "محتوى تعليمي للمبتدئين", "محتوى تقني متقدم",
|
| 189 |
+
"وصفة أو إرشادات عملية", "مراجعة منتج", "إعلان تجاري",
|
| 190 |
+
"نص قانوني رسمي", "نص طبي متخصص", "محتوى ترفيهي خفيف"],
|
| 191 |
+
),
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# "Which flavour of X" — the reference's `lang` preset generalised. The model invents the members.
|
| 195 |
+
SUBDOMAIN_FAMILIES = [
|
| 196 |
+
("لغة البرمجة المستخدمة في السؤال", "مبرمج يسأل عن لغة معينة"),
|
| 197 |
+
("نوع الوثيقة الرسمية المطلوبة", "مواطن يطلب مستندًا"),
|
| 198 |
+
("فرع التأمين المعني", "عميل يسأل عن وثيقته"),
|
| 199 |
+
("نوع الحساب البنكي المقصود", "عميل بنك"),
|
| 200 |
+
("تخصص الطبيب المطلوب", "مريض يبحث عن عيادة"),
|
| 201 |
+
("نوع العقار المطلوب", "باحث عن سكن"),
|
| 202 |
+
("نوع الشحنة المرسلة", "عميل شركة شحن"),
|
| 203 |
+
("مرحلة دراسية", "ولي أمر يسأل عن التسجيل"),
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
# v1's 18, plus 27 more.
|
| 207 |
+
DOMAINS = [
|
| 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 |
+
STYLES = {
|
| 256 |
+
"noun": ("كلمة واحدة أو كلمتين", "مثل: برمجة، فواتير، شكاوى"),
|
| 257 |
+
"phrase": ("عبارة قصيرة من ٢ إلى ٤ كلمات", "مثل: كتابة إبداعية، إلغاء الاشتراك"),
|
| 258 |
+
"desc": ("جملة وصفية كاملة", "مثل: أسئلة حول الفواتير والمدفوعات وطلبات الاسترجاع"),
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
DIFFICULTY = {
|
| 262 |
+
"easy": "اجعل الفئات متباعدة وواضحة الفروق بينها.",
|
| 263 |
+
"hard": ("اجعل بعض الفئات متقاربة جدًا في المعنى بحيث يصعب التمييز بينها دون قراءة النص "
|
| 264 |
+
"بعناية."),
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
# Mode 1: the model invents the lanes (topical / subdomain).
|
| 268 |
+
PROMPT_INVENT = """أنت تكتب بيانات تدريبية لنموذج يوجّه النصوص إلى فئات يحددها المستخدم.
|
| 269 |
+
|
| 270 |
+
المجال: {domain} ({hint})
|
| 271 |
+
|
| 272 |
+
الخطوة الأولى — اقترح {n} فئة للتوجيه في هذا المجال:
|
| 273 |
+
- اكتب كل فئة بأسلوب: {style_desc} ({style_ex}).
|
| 274 |
+
- {difficulty}
|
| 275 |
+
- الفئات يجب أن تغطي المجال دون تداخل كامل بينها.{other_note}
|
| 276 |
+
|
| 277 |
+
الخطوة الثانية — اكتب {k} نصوص مستقلة، كل نص من شخص حقيقي ({hint}).
|
| 278 |
+
النص رقم i يجب أن ينتمي إلى الفئة المطلوبة له في القائمة أدناه:
|
| 279 |
+
{targets}
|
| 280 |
+
|
| 281 |
+
{fmt}"""
|
| 282 |
+
|
| 283 |
+
# Mode 2/3: the lanes are given (single axis or mixed axes); the model only writes the texts.
|
| 284 |
+
PROMPT_GIVEN = """أنت تكتب بيانات تدريبية لنموذج يوجّه النصوص إلى فئات يحددها المستخدم.
|
| 285 |
+
|
| 286 |
+
الفئات المعطاة (استخدمها كما هي حرفيًا وبنفس الترتيب):
|
| 287 |
+
{lanes_block}
|
| 288 |
+
|
| 289 |
+
السياق: {hint}
|
| 290 |
+
|
| 291 |
+
اكتب {k} نصوص عربية مستقلة وواقعية. النص رقم i يجب أن ينتمي إلى الفئة المطلوبة له:
|
| 292 |
+
{targets}
|
| 293 |
+
|
| 294 |
+
{fmt}"""
|
| 295 |
+
|
| 296 |
+
FMT = """اكتب بهذا الشكل بالضبط:
|
| 297 |
+
|
| 298 |
+
{C}
|
| 299 |
+
(فئة في كل سطر، مسبوقة بـ "- "، بالترتيب وبنفس الصياغة في كل مرة)
|
| 300 |
+
{Q}
|
| 301 |
+
(النص العربي الأول، من سطر إلى ثلاثة أسطر)
|
| 302 |
+
{A}
|
| 303 |
+
(اسم الفئة التي ينتمي إليها هذا النص، منسوخًا حرفيًا من قائمة الفئات)
|
| 304 |
+
{Q}
|
| 305 |
+
(النص الثاني)
|
| 306 |
+
{A}
|
| 307 |
+
(فئته)
|
| 308 |
+
... وهكذا حتى {k} نصوص
|
| 309 |
+
{E}
|
| 310 |
+
|
| 311 |
+
قواعد مهمة:
|
| 312 |
+
- لا تذكر اسم الفئة داخل النص نفسه. النص يجب أن يدل على فئته بالمعنى لا بالكلمة.
|
| 313 |
+
- اكتب نصوصًا طبيعية كما يكتبها الناس فعلًا، بأطوال مختلفة.
|
| 314 |
+
- نوّع الأسماء والتفاصيل والأسلوب بين النصوص.
|
| 315 |
+
- لا تكتب أي شيء خارج الوسوم."""
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _targets_block(rng, n_lanes, has_other):
|
| 319 |
+
real = list(range(n_lanes))
|
| 320 |
+
targets = [rng.choice(real) for _ in range(ITEMS_PER_TASK)]
|
| 321 |
+
if has_other:
|
| 322 |
+
targets[rng.randrange(ITEMS_PER_TASK)] = n_lanes
|
| 323 |
+
return targets, "\n".join(f"- النص {i+1}: الفئة رقم {t+1}"
|
| 324 |
+
for i, t in enumerate(targets))
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def build_task(task_id: int, seed: int = 1234):
|
| 328 |
+
"""task_id -> (axes, prompt). Deterministic, so a resumed run redraws identical prompts."""
|
| 329 |
+
rng = random.Random(seed * 7_000_003 + task_id)
|
| 330 |
+
mode = rng.choices(["mixed", "axis", "topical"], weights=[45, 35, 20])[0]
|
| 331 |
+
style = rng.choice(["noun", "phrase", "phrase", "desc"])
|
| 332 |
+
difficulty = "hard" if rng.random() < 0.4 else "easy"
|
| 333 |
+
has_other = rng.random() < 0.30 # v1 used 0.15; the reference uses a catch-all often
|
| 334 |
+
other_lane = rng.choice(OTHER_LANES)
|
| 335 |
+
fmt = FMT.format(C=C, Q=Q, A=A, E=E, k=ITEMS_PER_TASK)
|
| 336 |
+
|
| 337 |
+
if mode == "topical":
|
| 338 |
+
use_sub = rng.random() < 0.2
|
| 339 |
+
domain, hint = rng.choice(SUBDOMAIN_FAMILIES if use_sub else DOMAINS)
|
| 340 |
+
n = rng.choice([2, 3, 3, 4, 4, 4, 5, 5, 6, 7, 8])
|
| 341 |
+
sd, sx = STYLES[style]
|
| 342 |
+
other_note = (f"\n- أضف في نهاية القائمة فئة أخيرة اسمها بالضبط: {other_lane}"
|
| 343 |
+
"\n- واجعل أحد النصوص لا ينتمي لأي فئة إلا هذه الفئة الأخيرة.") \
|
| 344 |
+
if has_other else ""
|
| 345 |
+
targets, tgt = _targets_block(rng, n, has_other)
|
| 346 |
+
axes = {"mode": mode, "axis": "subdomain" if use_sub else "topical", "domain": domain,
|
| 347 |
+
"style": style, "difficulty": difficulty, "has_other": has_other,
|
| 348 |
+
# total lane count, Other included - the axis/mixed branch counts the same way
|
| 349 |
+
"n_routes": n + (1 if has_other else 0), "targets": targets}
|
| 350 |
+
return axes, PROMPT_INVENT.format(domain=domain, hint=hint, n=n, k=ITEMS_PER_TASK,
|
| 351 |
+
style_desc=sd, style_ex=sx,
|
| 352 |
+
difficulty=DIFFICULTY[difficulty],
|
| 353 |
+
other_note=other_note, targets=tgt, fmt=fmt)
|
| 354 |
+
|
| 355 |
+
if mode == "axis":
|
| 356 |
+
name = rng.choice(list(AXES))
|
| 357 |
+
spec = AXES[name]
|
| 358 |
+
k = min(len(spec["lanes"]), rng.choice([3, 3, 4, 4, 5, 5, 6, 7]))
|
| 359 |
+
picked = rng.sample(spec["lanes"], k)
|
| 360 |
+
lanes = [rng.choice(w) for w in picked] # one wording per lane, per task
|
| 361 |
+
hint = spec["hint"]
|
| 362 |
+
axis_label, domain = name, name
|
| 363 |
+
else: # mixed - compound lanes from one pool
|
| 364 |
+
pool_name = rng.choice(list(COMPOUND_POOLS))
|
| 365 |
+
hint, pool = COMPOUND_POOLS[pool_name]
|
| 366 |
+
k = min(len(pool), rng.choice([3, 4, 4, 5, 5, 6, 6, 7, 8]))
|
| 367 |
+
lanes = rng.sample(pool, k)
|
| 368 |
+
axis_label, domain = "compound:" + pool_name, pool_name
|
| 369 |
+
|
| 370 |
+
if has_other:
|
| 371 |
+
lanes = lanes + [other_lane]
|
| 372 |
+
rng.shuffle(lanes)
|
| 373 |
+
n = len(lanes) - (1 if has_other else 0)
|
| 374 |
+
other_idx = lanes.index(other_lane) if has_other else None
|
| 375 |
+
real_idx = [i for i in range(len(lanes)) if i != other_idx]
|
| 376 |
+
targets = [rng.choice(real_idx) for _ in range(ITEMS_PER_TASK)]
|
| 377 |
+
if has_other:
|
| 378 |
+
targets[rng.randrange(ITEMS_PER_TASK)] = other_idx
|
| 379 |
+
tgt = "\n".join(f"- النص {i+1}: الفئة رقم {t+1}" for i, t in enumerate(targets))
|
| 380 |
+
lanes_block = "\n".join(f"- {c}" for c in lanes)
|
| 381 |
+
axes = {"mode": mode, "axis": axis_label, "domain": domain, "style": style,
|
| 382 |
+
"difficulty": difficulty, "has_other": has_other,
|
| 383 |
+
"n_routes": len(lanes), "targets": targets}
|
| 384 |
+
return axes, PROMPT_GIVEN.format(lanes_block=lanes_block, hint=hint, k=ITEMS_PER_TASK,
|
| 385 |
+
targets=tgt, fmt=fmt)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
# ---------------------------------------------------------------- parsing / validation
|
| 389 |
+
# Identical contract to router_common; the tag format and structural checks carry over unchanged.
|
| 390 |
+
|
| 391 |
+
BLOCK_RE = re.compile(re.escape(C) + r"(?P<cats>.*?)(?=" + re.escape(Q) + r")", re.S)
|
| 392 |
+
PAIR_RE = re.compile(re.escape(Q) + r"(?P<text>.*?)" + re.escape(A) + r"(?P<label>.*?)"
|
| 393 |
+
r"(?=" + re.escape(Q) + r"|" + re.escape(E) + r"|\Z)", re.S)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def norm(s: str) -> str:
|
| 397 |
+
s = unicodedata.normalize("NFKC", s or "")
|
| 398 |
+
s = re.sub(r"[ً-ْٰـ]", "", s)
|
| 399 |
+
return re.sub(r"\s+", " ", s).strip()
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def parse_items(raw: str):
|
| 403 |
+
m = BLOCK_RE.search(raw or "")
|
| 404 |
+
if not m:
|
| 405 |
+
return []
|
| 406 |
+
routes = []
|
| 407 |
+
for line in m.group("cats").split("\n"):
|
| 408 |
+
line = line.strip()
|
| 409 |
+
c = re.match(r"^[-*•]\s*(.+)$", line) or re.match(r"^\d+[.)]\s*(.+)$", line)
|
| 410 |
+
if c:
|
| 411 |
+
r = norm(c.group(1)).strip(" .:،")
|
| 412 |
+
if 2 <= len(r) <= 120:
|
| 413 |
+
routes.append(r)
|
| 414 |
+
if not routes:
|
| 415 |
+
return []
|
| 416 |
+
out = []
|
| 417 |
+
for p in PAIR_RE.finditer(raw):
|
| 418 |
+
out.append({"routes": routes, "text": norm(p.group("text")),
|
| 419 |
+
"label_text": norm(p.group("label")).strip(" .:،").lstrip("- ")})
|
| 420 |
+
return out
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def _match_route(label, routes):
|
| 424 |
+
if label in routes:
|
| 425 |
+
return routes.index(label)
|
| 426 |
+
hits = [i for i, r in enumerate(routes) if r and (r in label or label in r)]
|
| 427 |
+
return hits[0] if len(hits) == 1 else None
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def validate(item, min_text=15, max_text=600):
|
| 431 |
+
"""Structural only. Semantic correctness is decided by verify_router.py, not here."""
|
| 432 |
+
routes, text, label = item["routes"], item["text"], item["label_text"]
|
| 433 |
+
if not (2 <= len(routes) <= 9):
|
| 434 |
+
return False, "route_count"
|
| 435 |
+
if len(set(routes)) != len(routes):
|
| 436 |
+
return False, "duplicate_routes"
|
| 437 |
+
if not (min_text <= len(text) <= max_text):
|
| 438 |
+
return False, "text_length"
|
| 439 |
+
if any(t in text for t in (C, Q, A, E)):
|
| 440 |
+
return False, "tag_leak"
|
| 441 |
+
idx = _match_route(label, routes)
|
| 442 |
+
if idx is None:
|
| 443 |
+
return False, "label_not_in_routes"
|
| 444 |
+
if routes[idx] and routes[idx] in text:
|
| 445 |
+
return False, "lexical_giveaway"
|
| 446 |
+
if len(text.split()) < 4:
|
| 447 |
+
return False, "text_too_few_words"
|
| 448 |
+
return True, "ok"
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def label_index(item):
|
| 452 |
+
return _match_route(item["label_text"], item["routes"])
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def dedup_key(text: str) -> str:
|
| 456 |
+
return re.sub(r"\d+", "#", re.sub(r"\W+", "", norm(text)))
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def lane_set_key(routes) -> str:
|
| 460 |
+
return "|".join(sorted(norm(r) for r in routes))
|