File size: 14,790 Bytes
18fd039
 
 
 
 
 
 
 
 
 
 
 
22c2b18
 
 
 
 
 
18fd039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22c2b18
 
 
 
 
 
 
 
 
 
 
 
 
dfbdf81
 
 
22c2b18
dfbdf81
22c2b18
 
dfbdf81
 
 
 
 
 
22c2b18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18fd039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22c2b18
 
 
 
 
 
 
 
 
18fd039
 
 
 
 
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
"""
FilGoalBot — Intent Router
==========================
Pure regex-based intent detector. Lives in its own module so it can be
unit-tested without importing Groq, FAISS, or sentence-transformers.

Intents:
  1. lineup            → تشكيلة
  2. match_result      → نتيجة مباراة / أهداف
  3. transfer_news     → ميركاتو / انتقالات / عقود
  4. team_news         → أخبار فريق / مران / مؤتمر صحفي
  5. player_info       → معلومات لاعب / إحصائيات
  6. general_football  → ambiguous-but-still-football fallback
                         (e.g. "ترتيب الدوري", "موعد المباريات")
  7. out_of_scope      → clearly NOT football (weather, cooking, other
                         sports, politics …). The pipeline short-circuits
                         these with a tailored refusal BEFORE any
                         retrieval or LLM call.
"""

import re

INTENT_PATTERNS: dict[str, list[str]] = {
    "lineup": [
        r'تشكيل', r'تشكيله', r'التشكيلة',
        r'أساسي', r'اساسي',
        r'الحارس', r'الحراسه', r'حارس المرمى',
        r'خط الدفاع', r'خط الوسط', r'خط الهجوم',
        r'مين اللي هيلعب', r'مين هيلعب',
        r'الاحتياطي', r'البدلاء',
        r'الكابتن',
        r'مين هيبدأ', r'مين بيبدأ',
        r'الـ?11\b', r'الإحدى عشر',
    ],

    "match_result": [
        # MSA
        # (?<!يست) blocks "يستهدف" (transfer-targeting) from matching "هدف".
        r'نتيج', r'انتهت', r'انته', r'(?<!يست)هدف', r'أهداف', r'اهداف',
        r'فاز', r'خسر', r'تعادل', r'سجل', r'لقاء',
        r'ركلات الترجيح', r'بالركلات',
        r'الفوز', r'فوز', r'انتصار', r'انتصر', r'انتصرت',
        r'هزيمة', r'هزم', r'انهزم',
        r'تأهل', r'تأهلت', r'تأهلوا',  # qualified (from a match)
        r'دربي',  # derby — implies a specific match
        # Allow 1-3 tokens between "فعل" and "أمام/ضد" so multi-word team
        # names like "الهلال السوداني" match.
        r'ماذا فعل\s+\S+(?:\s+\S+){0,2}\s+(أمام|ضد)',
        # dialect
        r'سكور', r'بالكام', r'جول', r'جوالات',
        r'كسب', r'اتكسب', r'اتعادل', r'اتغلب', r'ربح',
        r'فاز إيه', r'إيه نتيجة', r'ايه النتيجة',
        r'خلصت', r'خلص.*مباراة', r'مباراة.*امس', r'امتى خلص',
        r'دلوقتي.*نتيج', r'نتيج.*دلوقتي',
        r'بالرك',
        r'الفايز', r'فايز',
        r'مين كسب', r'مين فاز', r'مين خسر',
        r'بكام', r'بكم',
    ],

    "player_info": [
        r'لاعب', r'إحصائي', r'احصائي',
        r'إصاب', r'اصاب', r'مسيرة', r'جنسي', r'عمر',
        r'جاهز', r'غايب', r'موجود', r'مشارك',
        r'هيرجع امتى', r'رجع امتى', r'امتى بيرجع',
        r'اتاذى', r'تأذى', r'مصاب',
        r'اشترك', r'نزل من الاحتياطي',
        r'هيلعب', r'بيلعب', r'ماعرك', r'عارك',
        r'فين.*\b(صلاح|محمد|علي|عمر|احمد|كريم|مصطفى|حسام|إمام|زلاكه|شيكابالا|افشه|أفشة|بنشرقي|منسي)\b',
        r'ايه اخبار',
        # Allow 1-2 tokens between "أخبار" and "في/مع/ضد" — single-token form
        # missed compound names like "محمد صلاح".
        r'أخبار\s+\w+(?:\s+\w+)?\s+(في|مع|ضد)\s+',
        r'يتعافى', r'متعافى', r'تعافى',
        r'متى يعود', r'متى رجع',
        r'مستواه', r'أداءه', r'أدائه', r'أداء',
        r'حالته', r'موقفه',
        # MSA: questions about a player's status/career
        r'ما حالة', r'الصحي', r'البدني',
        r'إنجاز', r'الإنجاز',
        r'عقوبة',  # disciplinary actions → about the player
        # "(?:^|\s)" anchors so "حكم" doesn't match "كم" inside it.
        r'(?:^|\s)كم مباراة', r'(?:^|\s)كم مرة',
        r'سيغيب', r'يغيب',
        r'إضراب',  # player-specific action (e.g. Ronaldo's training strike)
        r'موقفه', r'موقف\s+\S+\s+من',  # "X's stance on..."
    ],

    "team_news": [
        r'مران', r'تدريب', r'محاضر', r'مؤتمر',
        r'جهاز', r'الجهاز الفني', r'مدرب', r'قائد',
        r'اجتماع', r'صرح', r'علق', r'تصريح', r'تصريحات',
        r'تحضير', r'استعداد',
        r'بيان', r'الإدارة', r'يدعم', r'دعم.*منظومة',
        # (?!\w) blocks suffix matches: without it "المصري" would match
        # "المصرية" and pull generic Egypt-football queries into team_news.
        r'أخبار.*(الأهلي|الزمالك|بيراميدز|الإسماعيلي|المصري|سيراميكا|طلائع|فاركو|الجونة|سموحة|المقاولون|إنبي|البنك الأهلي|غزل المحلة|حرس الحدود|مودرن)(?!\w)',
        r'أخبار.*فريق',
        r'بيعمل', r'قال إيه', r'قال ايه',
        r'بيحصل', r'اللي بيحصل',
        # MSA quote/statement forms — "what did X say" / "what did X announce".
        r'ماذا قال', r'ما الذي قاله',
        r'ماذا أعلن', r'ماذا حدث في\s+(?!مباراة|دربي|لقاء)',  # not match-context
    ],

    "transfer_news": [
        r'ميركاتو', r'انتقال', r'صفق', r'عقد', r'رحيل',
        r'ضم', r'انتقل', r'تعاقد', r'إعار', r'اعار',
        r'مفاوضات', r'فسخ', r'مجاني',
        r'مدته', r'عقده.*بيخلص', r'بيخلص.*عقده',
        r'اوبشن', r'أوبشن',
        r'هيجدد', r'هيجيب', r'هيضم', r'هيروح',
        r'هيفضل', r'مش هيفضل',
        r'جه جديد', r'جاي جديد',
        r'جاب مين', r'جابوا مين',
        r'راح فين', r'رحل لـ',
        # Coach hires are personnel acquisitions in this taxonomy — see test set.
        r'المدرب الجديد', r'مدرب جديد',
        r'لاعب جديد', r'لاعبين جدد',
        r'استعار', r'يستعير', r'استعارة',
        r'يرحل', r'سيرحل', r'هيرحل',
        r'هيوقع', r'يوقع.*(عقد|للـ|مع)',
        r'وقع.*عقد', r'وقع.*للـ', r'وقع.*مع',
        r'تجديد',
        # Coach personnel changes are transfers in this taxonomy (see test set:
        # "غزل المحلة ضم مدرب جديد" / "مين المدرب الجديد للمنتخب المصري").
        r'استقال', r'استقالة', r'إقالة', r'أقال', r'أقيل',
        r'يستهدف', r'استهدف',
        r'اتفاق مع',
        # Allow 1-3 tokens between the verb and direction so multi-word names
        # like "جيمس رودريجز" or "عزمي غومة" match. Plain "ل" suffix accepted
        # because the corpus uses both "لـ" and "للنادي" forms.
        r'وصل\s+\S+(?:\s+\S+){0,2}\s+(?:ل|إلى)',
        r'عاد\s+\S+(?:\s+\S+){0,2}\s+(?:إلى|ل)\s+\S+',  # player rejoining a club
    ],
}

# Order matters: lineup checked FIRST because "تشكيل" is very specific and would
# also leak into team_news. Then transfer_news to catch contract/مفاوضات before
# team_news's broad "مدرب" pattern. player_info is last so generic player names
# don't outrank a more specific lineup/result query.
INTENT_ORDER = ["lineup", "match_result", "transfer_news", "team_news", "player_info"]

# Extractive intents — answer is usually a direct fact in one chunk.
# These can use the cheaper 8B model without quality loss.
EXTRACTIVE_INTENTS = {"lineup", "match_result"}


# Patterns that signal a query is clearly NOT about football. Checked only as
# a tiebreaker against the general_football fallback — i.e. when no specific
# football intent fires. Otherwise legitimate borderline football queries
# (e.g. "ترتيب الدوري المصري") would over-refuse.
#
# Each pattern is chosen to be either (a) syntactically specific enough not
# to collide with football vocabulary, or (b) reference a topic with no
# football overlap at all. Where collision is possible (a club president
# vs. a country president), the pattern requires the non-football
# disambiguator explicitly.
OUT_OF_SCOPE_PATTERNS: list[str] = [
    # Weather
    r'\bالطقس\b', r'حال[ةه] الجو', r'درج[ةه] الحرار[ةه]', r'الأمطار',
    # Cooking / food — bare "وصفة" too, since recipe queries don't always
    # spell out "وصفة طبخ" (e.g. "ما هي وصفة الكشري؟"). Football queries
    # never use "وصفة" so the broader pattern is safe.
    r'وصف[ةه] طبخ', r'طريق[ةه] طبخ', r'\bطبخ', r'مطبخ',
    r'\bوصف[ةه]\b',
    # Restaurants / dining
    r'\bمطعم\b', r'\bمطاعم\b',
    # Geography / demographics — only when phrased as a definition-style
    # question ("ما عاصمة X؟", "عدد سكان X"). The interrogative anchor
    # avoids collisions with football mentions like "العاصمة الإدارية"
    # (a stadium location) or surnames containing "سكان".
    r'(?:ما|إيه|ايه)\s+عاصمة',
    r'عدد\s+سكان',
    # Other sports — explicit so "كرة" alone doesn't trip
    r'كر[ةه]\s+(?:السل[ةه]|الطائر[ةه]|اليد)',
    r'\bتنس\b', r'ملاكم[ةه]', r'سباح[ةه]', r'ألعاب القوى',
    r'فورمولا', r'دراج[ةه] هوائي[ةه]', r'شطرنج',
    # Named non-football tournaments — covers cases where the query
    # uses match_result vocabulary ("من فاز ببطولة …") but the subject
    # is a non-football tournament that bypasses the كرة-based check above.
    r'ويمبلدون', r'رولان\s+جاروس', r'\bNBA\b', r'\bالـ?NBA\b',
    r'يورو\s*فيجن', r'الأوسكار',
    # Politics — require qualifiers that don't apply to club officials
    r'رئيس الجمهوري[ةه]', r'الانتخابات الرئاسي[ةه]', r'البرلمان',
    r'وزير\s+(?:الخارجي[ةه]|الداخلي[ةه]|الصح[ةه]|التعليم|المالي[ةه])',
    # Tech / general apps
    r'برمج[ةه]', r'كمبيوتر', r'لاب\s*توب', r'هاتف محمول', r'موقع إلكتروني',
    # Science / academia — extremely unlikely to collide with football
    r'نظري[ةه] النسبي[ةه]', r'\bفيزياء\b', r'\bكيمياء\b', r'\bرياضيات\b',
    r'\bأينشتاين\b', r'\bنيوتن\b', r'الجاذبي[ةه]', r'\bالذر[ةه]\b',
    # Astronomy
    r'\bالشمس\b', r'\bالقمر\b', r'الكواكب', r'\bالمجر[ةه]\b',
    # Distance / measurement questions — "كم تبعد X عن Y" is geographic,
    # nothing in football reads that way.
    r'\bتبعد\b.{0,30}\bعن\b',
    # Medicine — pair "علاج" with a disease so we don't fire on "علاج إصابة
    # محمد صلاح". The disease list covers the common test-set probes.
    r'علاج\s+(?:السكري|الضغط|البرد|نزلات|الكوليسترول|السرطان|الإيدز)',
    r'نزلات\s+البرد', r'مرض\s+السكري', r'الكوليسترول',
    # History / institutions — "متى تأسست" + a non-football institution
    r'متى\s+تأسست\s+(?:جامع[ةه]|كلي[ةه]|الدول[ةه]|البنك|الجمهوري[ةه])',
    r'\bجامع[ةه]\s+(?:الأزهر|القاهر[ةه]|عين شمس|الإسكندري[ةه])',
    # Creative writing / arts
    r'\bقصيد[ةه]\b', r'اكتب\s+لي\s+قصة', r'اكتب\s+لي\s+مقال',
    r'\bشعر\s+عن\b', r'\bرواي[ةه]\b\s+\S+',
    # Entertainment. Religion patterns dropped — "رمضان", "الصلاة" and
    # "الصيام" collide with legitimate football queries ("هل سيغيب صلاح
    # في رمضان؟", "وقت الصلاة قبل المباراة"). The test set has no
    # religion-refusal cases that need them.
    r'فيلم\s+\S+', r'مسلسل\s+\S+', r'ممثل\s+\S+', r'مطرب\s+\S+',
    # Economy (non-football)
    r'سعر\s+الدولار', r'سعر\s+الذهب', r'سعر\s+سهم', r'سعر\s+النفط',
    r'سعر\s+الفائد[ةه]', r'البورص[ةه]', r'التضخم',
]


def _is_out_of_scope(query: str) -> bool:
    return any(re.search(p, query) for p in OUT_OF_SCOPE_PATTERNS)


def detect_intent(query: str) -> str:
    q = query.lower()
    # "ماذا قال X" → team_news. Override needed because match_result runs first,
    # and quoted statements often mention "فوز", "هدف" etc that fire there.
    if re.search(r'ماذا\s+قال|ما\s+الذي\s+قاله|ماذا\s+أعلن', q):
        return "team_news"
    # High-priority override: "لاعب اسمه ..." routes to player_info to defeat
    # match_result's "سجل" trigger (e.g. "كم سجل لاعب اسمه X"). Skip the override
    # when a transfer verb is present — "هل انتقل لاعب اسمه X" is transfer_news.
    if re.search(r'لاعب\s+اسمه', q) and not re.search(r'انتقل|صفق|مفاوضات|تعاقد|إعار|اعار', q):
        return "player_info"
    # Player-centric return-to-training. Allow 1-3 tokens between "عاد" and
    # "ل/إلى تدريب" so multi-word names like "دي بروين" / "عبد المنعم" match.
    # Without this override, team_news's "تدريب" pattern swallows the case.
    if re.search(r'(?:عاد|عودة)\s+\S+(?:\s+\S+){0,2}\s+ل[إا]?\s*تدريب', q):
        return "player_info"
    # Out-of-scope check BEFORE INTENT_ORDER. Football vocabulary is so
    # broad (نتيجة، مباراة، حالة، ...) that a query like
    # "نتيجة مباراة كرة السلة" would otherwise match match_result on
    # "نتيج" and lose its OOS signal. The patterns are conservative —
    # they target topics with no overlap with football (weather, other
    # sports, politics-with-disambiguator, etc.) — so promoting them
    # over INTENT_ORDER does not over-refuse genuine football queries.
    if _is_out_of_scope(q):
        return "out_of_scope"
    for intent in INTENT_ORDER:
        for pattern in INTENT_PATTERNS[intent]:
            if re.search(pattern, q):
                return intent
    return "general_football"