Mr-Help commited on
Commit
289306a
·
verified ·
1 Parent(s): a722165

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -832
app.py CHANGED
@@ -1,865 +1,108 @@
1
  import os
2
- import re
3
- from typing import Optional
4
 
5
- import openai
6
- from dotenv import load_dotenv
7
- from fastapi import FastAPI, Request
8
- from fastapi.responses import HTMLResponse, JSONResponse
9
- from pydantic import BaseModel
10
 
11
- # =========================
12
- # Environment / Model Setup
13
- # =========================
14
 
15
- load_dotenv()
16
- api_key = os.environ.get("HUGGINGFACEHUB_API_TOKEN")
17
 
18
- model_link = "meta-llama/Meta-Llama-3-8B-Instruct"
19
- base_url = "https://router.huggingface.co/v1"
20
 
21
- app = FastAPI()
 
 
 
22
 
23
 
24
- class Message(BaseModel):
25
- message: str
26
-
27
-
28
- @app.get("/", response_class=HTMLResponse)
29
- async def read_root():
30
- return "Welcome to Up to 12 Chat Processor"
31
-
32
-
33
- @app.get("/health")
34
- async def health():
35
- return {"ok": True, "service": "up"}
36
-
37
-
38
- # =========================
39
- # Intents
40
- # =========================
41
-
42
- INTENTS = {
43
- "GREETING": "تحية/بداية محادثة",
44
- "COURSES_MENU": "طلب قائمة الكورسات",
45
- "COURSE_TYPE_DETAILS": "سؤال عن نوع كورس محدد",
46
- "CENTER_INFO": "سؤال عن المركز",
47
- "CHILDREN_COURSES": "كورس أطفال",
48
- "ONLINE_COURSES": "كورسات أونلاين",
49
- "WEEKEND_COURSES": "كورسات ويكند",
50
- "HUMAN_AGENT": "طلب خدمة عملاء/موظف",
51
- "OTHER": "غير معروف/خارج النطاق",
52
- }
53
-
54
-
55
- # =========================
56
- # Knowledge Base
57
- # =========================
58
-
59
- KB = {
60
- "center": {
61
- "name": "ÄDK - Egyptian-German Cultural Centre",
62
- "established": "1998",
63
- "branches": "له فروع مختلفة في القاهرة",
64
- "adults_levels": "للكبار من A1 لحد C1",
65
- "children_levels": "للأطفال من A1 لحد B1",
66
- "goethe": "أول معهد معتمد من Goethe Institute في الشرق الأوسط وشمال أفريقيا",
67
- },
68
- "courses": {
69
- "express": {
70
- "title": "Express Courses",
71
- "details": [
72
- "كورسات Super intensive لتعلم الألماني بسرعة وكفاءة",
73
- "النظام ده Challenging وبيحتاج مجهود",
74
- "ممكن تحجز Stage كاملة (A1 - A2 - B1)",
75
- "3 محاضرات في الأسبوع",
76
- "كل محاضرة 4 ساعات",
77
- "مدة الـ stage شهرين ونص",
78
- ],
79
- },
80
- "intensive": {
81
- "title": "Intensive Courses",
82
- "details": [
83
- "ممكن تحجز Stage كاملة (A1 - A2 - B1)",
84
- "محاضرتين في الأسبوع",
85
- "كل محاضرة 4 ساعات",
86
- "مدة الـ stage 3 شهور ونص",
87
- ],
88
- },
89
- "regular": {
90
- "title": "Regular Courses",
91
- "details": [
92
- "ممكن تحجز كل level لوحده من A1.1 لحد B1.3",
93
- "محاضرتين في الأسبوع",
94
- "كل محاضرة 4 ساعات",
95
- "مدة الكورس شهر ونص",
96
- ],
97
- },
98
- "weekend": {
99
- "title": "Weekend Courses",
100
- "details": [
101
- "مناسبة للناس اللي مش فاضيين خلال الأسبوع بسبب شغل أو دراسة",
102
- "بتتبع نفس outline وساعات الـ Regular",
103
- ],
104
- },
105
- "online": {
106
- "title": "Online Courses",
107
- "details": [
108
- "متاحة بنظام أونلاين",
109
- "الحضور بيكون online",
110
- ],
111
- },
112
- "children": {
113
- "title": "Children Courses",
114
- "details": [
115
- "من سن 5 سنين لحد 15 سنة",
116
- "الهدف منها تنمية مهارات الطفل واللغة",
117
- "وكمان تشجيع التفكير الحر والنقدي والاستقلالية",
118
- ],
119
- },
120
- },
121
- }
122
-
123
-
124
- # =========================
125
- # Helpers
126
- # =========================
127
-
128
- def normalize_arabic(text: str) -> str:
129
- if not text:
130
- return ""
131
-
132
- text = text.strip().lower()
133
-
134
- replacements = {
135
- "أ": "ا",
136
- "إ": "ا",
137
- "آ": "ا",
138
- "ة": "ه",
139
- "ى": "ي",
140
- "ؤ": "و",
141
- "ئ": "ي",
142
- }
143
-
144
- for old, new in replacements.items():
145
- text = text.replace(old, new)
146
-
147
- return text
148
-
149
-
150
- def detect_gender(text: str) -> Optional[str]:
151
- """
152
- Returns:
153
- - 'male'
154
- - 'female'
155
- - None
156
- """
157
- t = normalize_arabic(text)
158
-
159
- female_markers = [
160
- "انا مهتمه",
161
- "مهتمه",
162
- "عايزه",
163
- "عاوزه",
164
- "عاوزة",
165
- "حابه",
166
- "حابة",
167
- "محتاجه",
168
- "محتاجة",
169
- "عايزه اقدم",
170
- "عايزه اعرف",
171
- "عايزه اسال",
172
- "عايزة",
173
- "عايزة اعرف",
174
- "عايزة اقدم",
175
- "عايزة اسال",
176
- "انا ام",
177
- "انا ماما",
178
- "انا والده",
179
- "بنتي",
180
- "لبنتي",
181
- ]
182
-
183
- male_markers = [
184
- "انا مهتم",
185
- "مهتم",
186
- "عايز",
187
- "حابب",
188
- "محتاج",
189
- "عايز اقدم",
190
- "عايز اعرف",
191
- "عايز اسال",
192
- "انا اب",
193
- "انا بابا",
194
- "انا والد",
195
- "ابني",
196
- "لابني",
197
- ]
198
-
199
- # female first to avoid "مهتم" matching inside "مهتمه"
200
- for marker in female_markers:
201
- if marker in t:
202
- return "female"
203
-
204
- for marker in male_markers:
205
- if marker in t:
206
- return "male"
207
-
208
- return None
209
-
210
-
211
- def choose_variant(gender: Optional[str], male_text: str, female_text: str, neutral_text: str) -> str:
212
- if gender == "male":
213
- return male_text
214
- if gender == "female":
215
- return female_text
216
- return neutral_text
217
-
218
-
219
- def detect_intent(text: str) -> str:
220
- t = normalize_arabic(text)
221
-
222
- # 1) Human agent first
223
- if any(
224
- x in t
225
- for x in [
226
- "خدمه العملاء",
227
- "خدمة العملاء",
228
- "موظف",
229
- "حد يرد",
230
- "اكلم",
231
- "اتواصل",
232
- "support",
233
- "agent",
234
- ]
235
- ):
236
- return "HUMAN_AGENT"
237
-
238
- # 2) Specific course types first
239
- if any(x in t for x in ["online", "اونلاين", "اون لاين", "zoom", "زووم", "من البيت"]):
240
- return "ONLINE_COURSES"
241
-
242
- if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطله", "عطلة"]):
243
- return "WEEKEND_COURSES"
244
-
245
- if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سنين", "سن كام", "سن قد ايه"]):
246
- return "CHILDREN_COURSES"
247
-
248
- if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
249
- return "COURSE_TYPE_DETAILS"
250
-
251
- if any(x in t for x in ["intensive", "مكثف", "مكثفه"]):
252
- return "COURSE_TYPE_DETAILS"
253
-
254
- if any(x in t for x in ["regular", "ريجولار", "منتظم"]):
255
- return "COURSE_TYPE_DETAILS"
256
-
257
- # 3) Center info
258
- if any(
259
- x in t
260
- for x in [
261
- "المركز",
262
- "adk",
263
- "ädk",
264
- "اتاسس",
265
- "اتأسس",
266
- "تاريخ",
267
- "goethe",
268
- "معتمد",
269
- "نبذه",
270
- "نبذة",
271
- ]
272
- ):
273
- return "CENTER_INFO"
274
-
275
- # 4) General courses
276
- if any(
277
- x in t
278
- for x in [
279
- "كورسات",
280
- "دورات",
281
- "courses",
282
- "الكورسات المتاحه",
283
- "الكورسات المتاحة",
284
- "عايز كورس",
285
- "عايزه كورس",
286
- "عايز اتعلم",
287
- "عايزه اتعلم",
288
- "الماني عندكم",
289
- "ألماني",
290
- "الماني",
291
- ]
292
- ):
293
- return "COURSES_MENU"
294
-
295
- # 5) Greeting
296
- if re.search(r"\b(hi|hello|hey|start)\b", t) or any(
297
- x in t for x in ["اهلا", "السلام", "هاي", "مرحبا", "ابدأ", "ابدء", "مسا", "صباح الخير"]
298
- ):
299
- return "GREETING"
300
-
301
- return "OTHER"
302
-
303
-
304
- def detect_course_subtype(text: str) -> Optional[str]:
305
- t = normalize_arabic(text)
306
-
307
- if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
308
- return "express"
309
- if any(x in t for x in ["intensive", "مكثف", "مكثفه"]):
310
- return "intensive"
311
- if any(x in t for x in ["regular", "ريجولار", "منتظم"]):
312
- return "regular"
313
- if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطله", "عطلة"]):
314
- return "weekend"
315
- if any(x in t for x in ["online", "اونلاين", "اون لاين", "zoom", "زووم", "من البيت"]):
316
- return "online"
317
- if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سنين", "سن كام", "سن قد ايه"]):
318
- return "children"
319
-
320
- return None
321
-
322
-
323
- def detect_other_subtype(text: str) -> Optional[str]:
324
- t = normalize_arabic(text)
325
-
326
- if any(x in t for x in ["العنوان", "مكانكم فين", "مكانكو فين", "فين المكان", "لوكيشن", "الموقع فين"]):
327
- return "ADDRESS_QUERY"
328
-
329
- if any(x in t for x in ["مواعيد", "ميعاد", "الميعاد", "امتى", "متى", "الفتره", "الفترة", "صباحي", "مسائي", "بعد الظهر", "بالليل"]):
330
- return "SCHEDULE_QUERY"
331
-
332
- if any(x in t for x in ["التقديم", "اقدم", "اقدملكم", "اسجل", "اسجل ازاي", "التسجيل", "التحاق", "احجز", "الحجز"]):
333
- return "REGISTRATION_QUERY"
334
-
335
- if any(x in t for x in ["السعر", "الاسعار", "الأسعار", "بكام", "كم", "التكلفه", "التكلفة", "الرسوم"]):
336
- return "PRICE_QUERY"
337
-
338
- if any(x in t for x in ["فروع", "فرع", "branch", "branches"]):
339
- return "BRANCH_QUERY"
340
-
341
- if any(x in t for x in ["رقم", "واتساب", "تليفون", "اتواصل", "التواصل", "اكلم"]):
342
- return "CONTACT_QUERY"
343
-
344
- return None
345
-
346
-
347
- def fallback_customer_service_reply(gender: Optional[str] = None) -> str:
348
- line2 = choose_variant(
349
- gender,
350
- "تحب أوجّه استفسارك لفريق خدمة العملاء؟",
351
- "تحبي أوجّه استفسارك لفريق خدمة العملاء؟",
352
- "تحب أو تحبي أوجّه استفسارك لفريق خدمة العملاء؟"
353
- )
354
- return (
355
- "تمام — عشان نضمن لك إجابة دقيقة، فريق خدمة العملاء هيقدر يفيدك أكتر.\n"
356
- f"{line2}"
357
- )
358
-
359
-
360
- def build_courses_menu_reply(gender: Optional[str] = None) -> str:
361
- ask_line = choose_variant(
362
- gender,
363
- "تحب تعرف تفاصيل أنهي نوع؟",
364
- "تحبي تعرفي تفاصيل أنهي نوع؟",
365
- "تحب تعرف تفاصيل أنهي نوع؟"
366
- )
367
- return (
368
- "عندنا الأنواع دي من كورسات الألماني:\n"
369
- "Regular / Intensive / Express / Weekend / Online / Children\n"
370
- f"{ask_line}"
371
- )
372
-
373
-
374
- def build_center_info_reply(gender: Optional[str] = None) -> str:
375
- center = KB["center"]
376
- closing = choose_variant(
377
- gender,
378
- "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء.",
379
- "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء.",
380
- "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء."
381
- )
382
- return (
383
- f"ÄDK اتأسس سنة {center['established']}، و{center['branches']}.\n"
384
- f"الكورسات للكبار {center['adults_levels']}، وللأطفال {center['children_levels']}.\n"
385
- f"وكمان هو {center['goethe']}.\n"
386
- f"{closing}"
387
- )
388
-
389
-
390
- def build_course_details_reply(course_key: str, gender: Optional[str] = None) -> str:
391
- details = KB["courses"][course_key]["details"]
392
-
393
- if course_key == "express":
394
- ask_line = choose_variant(
395
- gender,
396
- "تحب تعرف الفرق بينه وبين الـ Intensive؟",
397
- "تحبي تعرفي الفرق بينه وبين الـ Intensive؟",
398
- "تحب تعرف الفرق بينه وبين الـ Intensive؟"
399
- )
400
- start_line = choose_variant(
401
- gender,
402
- "الـ Express مناسب لو عايز تتعلم بسرعة.",
403
- "الـ Express مناسب لو عايزة تتعلمي بسرعة.",
404
- "الـ Express مناسب لو حابب أو حابة تتعلم بسرعة."
405
- )
406
- return (
407
- f"{start_line}\n"
408
- f"{details[0]}، و{details[1]}.\n"
409
- f"ممكن تحجز Stage كاملة، والنظام {details[3]}، {details[4]}.\n"
410
- f"مدة الـ stage {details[5].replace('مدة الـ stage ', '')}.\n"
411
- f"{ask_line}"
412
- )
413
-
414
- if course_key == "intensive":
415
- ask_line = choose_variant(
416
- gender,
417
- "تحب أقارن لك بينه وبين الـ Regular؟",
418
- "تحبي أقارن لك بينه وبين الـ Regular؟",
419
- "تحب أقارن لك بينه وبين الـ Regular؟"
420
- )
421
- return (
422
- "الـ Intensive ممكن تحجز فيه Stage كاملة من A1 لحد B1.\n"
423
- f"النظام {details[1]}، و{details[2]}.\n"
424
- f"مدة الـ stage {details[3].replace('مدة الـ stage ', '')}.\n"
425
- f"{ask_line}"
426
- )
427
-
428
- if course_key == "regular":
429
- ask_line = choose_variant(
430
- gender,
431
- "تحب تعرف لو ده أنسب لك ولا الـ Intensive؟",
432
- "تحبي تعرفي لو ده أنسب لك ولا الـ Intensive؟",
433
- "تحب تعرف لو ده أنسب لك ولا الـ Intensive؟"
434
- )
435
- start_line = choose_variant(
436
- gender,
437
- "الـ Regular مناسب لو تحب تمشي level level.",
438
- "الـ Regular مناسب لو تحبي تمشي level level.",
439
- "الـ Regular مناسب لو حابب أو حابة تمشي level level."
440
- )
441
- return (
442
- f"{start_line}\n"
443
- f"ممكن تحجز كل level لوحده من A1.1 لحد B1.3.\n"
444
- f"النظام {details[1]}، و{details[2]}.\n"
445
- f"مدة الكورس {details[3].replace('مدة الكورس ', '')}.\n"
446
- f"{ask_line}"
447
- )
448
-
449
- if course_key == "weekend":
450
- ask_line = choose_variant(
451
- gender,
452
- "تحب حضور في الويكند ولا كنت بتفكر في أونلاين؟",
453
- "تحبي حضور في الويكند ولا كنتِ بتفكري في أونلاين؟",
454
- "تحب حضور في الويكند ولا كنت بتفكر في أونلاين؟"
455
- )
456
- return (
457
- "الـ Weekend مناسب لو إنت مشغول خلال الأسبوع بسبب شغل أو دراسة.\n"
458
- "وبيمشي بنفس outline وساعات الـ Regular.\n"
459
- f"{ask_line}"
460
- )
461
-
462
- if course_key == "online":
463
- ask_line = choose_variant(
464
- gender,
465
- "تحب أقولك على الأنواع المتاحة؟",
466
- "تحبي أقولك على الأنواع المتاحة؟",
467
- "تحب أقولك على الأنواع المتاحة؟"
468
- )
469
- return (
470
- "عندنا كورسات أونلاين، والحضور بيكون online.\n"
471
- "ولو مناسب لك النظام ده، أقدر أساعدك تختار النوع الأقرب لاحتياجك.\n"
472
- f"{ask_line}"
473
- )
474
-
475
- if course_key == "children":
476
- ask_line = choose_variant(
477
- gender,
478
- "تحب أعرفك على نظام الأطفال بشكل أقرب؟",
479
- "تحبي أعرفك على نظام الأطفال بشكل أقرب؟",
480
- "تحب أعرفك على نظام الأطفال بشكل أقرب؟"
481
- )
482
- return (
483
- "عندنا كورسات أطفال من سن 5 لحد 15 سنة.\n"
484
- "الهدف منها تنمية مهارات الطفل واللغة.\n"
485
- "وكمان بتشجع التفكير الحر والنقدي والاستقلالية.\n"
486
- f"{ask_line}"
487
- )
488
-
489
- return fallback_customer_service_reply(gender)
490
-
491
-
492
- def generate_other_reply(user_text: str, gender: Optional[str] = None) -> str:
493
- subtype = detect_other_subtype(user_text)
494
-
495
- if subtype == "ADDRESS_QUERY":
496
- ask_line = choose_variant(
497
- gender,
498
- "تحب أبلّغهم إنك مهتم بالحضور؟",
499
- "تحبي أبلّغهم إنك مهتمة بالحضور؟",
500
- "تحب أبلّغهم إنك مهتم أو مهتمة بالحضور؟"
501
- )
502
- return (
503
- "تمام، بالنسبة للعنوان ده بيكون خاص بالكورسات الحضوري.\n"
504
- "أقدر أوصّل استفسارك لفريق خدمة العملاء عشان يبعثوا لك الفرع المناسب.\n"
505
- f"{ask_line}"
506
- )
507
-
508
- if subtype == "SCHEDULE_QUERY":
509
- line2 = choose_variant(
510
- gender,
511
- "قولّي تفضّل أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء.",
512
- "قولّي تفضّلي أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء.",
513
- "قولّي تفضّل أونلاين ولا حضور، وصباحي ولا مسائي، وأنا أوجّه استفسارك بشكل أوضح لخدمة العملاء."
514
- )
515
- return (
516
- "تمام، المواعيد بتختلف حسب إذا كنت مهتم بأونلاين أو حضور، وكمان حسب الفترة المناسبة لك.\n"
517
- f"{line2}"
518
- )
519
 
520
- if subtype == "REGISTRATION_QUERY":
521
- ask_line = choose_variant(
522
- gender,
523
- "تحب التقديم لكورس أونلاين ولا حضور؟",
524
- "تحبي التقديم لكورس أونلاين ولا حضور؟",
525
- "تحب التقديم لكورس أونلاين ولا حضور؟"
526
- )
527
- return (
528
- "تمام، أقدر أوجّه استفسارك لفريق خدمة العملاء عشان يوضحوا لك خطوات التقديم أو التسجيل بشكل دقيق.\n"
529
- f"{ask_line}"
530
- )
531
 
532
- if subtype == "PRICE_QUERY":
533
- line2 = choose_variant(
534
- gender,
535
- "قولّي مهتم بأنهي نوع كورس، وأنا أوجّه استفسارك لخدمة العملاء بشكل أوضح.",
536
- "قولّي مهتمة بأنهي نوع كورس، وأنا ��وجّه استفسارك لخدمة العملاء بشكل أوضح.",
537
- "قولّي مهتم بأنهي نوع كورس، وأنا أوجّه استفسارك لخدمة العملاء بشكل أوضح."
538
- )
539
- return (
540
- "تمام، الأسعار بتختلف حسب نوع الكورس والنظام المناسب لك.\n"
541
- f"{line2}"
542
- )
543
-
544
- if subtype == "BRANCH_QUERY":
545
- ask_line = choose_variant(
546
- gender,
547
- "تحب أبلّغهم إنك مهتم بكورس حضور؟",
548
- "تحبي أبلّغهم إنك مهتمة بكورس حضور؟",
549
- "تحب أبلّغهم إنك مهتم بكورس حضور؟"
550
- )
551
- return (
552
- "تمام، أقدر أوجّه استفسارك لفريق خدمة العملاء عشان يوضحوا لك الفرع المناسب.\n"
553
- f"{ask_line}"
554
- )
555
-
556
- if subtype == "CONTACT_QUERY":
557
- return (
558
- "تمام، أقدر أوصّل استفسارك لفريق خدمة العملاء.\n"
559
- "قولّي بس سؤالك عن الكورسات، التقديم، المواعيد، ولا الحضور؟"
560
- )
561
 
562
- return fallback_customer_service_reply(gender)
563
 
 
 
 
 
 
 
564
 
565
- def sanitize_reply(text: str, gender: Optional[str] = None) -> str:
566
- if not text or not text.strip():
567
- return fallback_customer_service_reply(gender)
568
-
569
- text = text.strip()
570
 
571
- replacements = {
572
- "حضرتك": "إنت",
573
- "مرحبا": "أهلا",
574
- "مرحباً": "أهلا",
575
- "يسعدني": "أقدر",
576
- "يهتم بيك": "تحب",
577
- "اقترحك": "أقترح لك",
578
- "يمكنني": "أقدر",
579
- "العميل": "إنت",
580
- "المستخدم": "إنت",
581
  }
582
 
583
- for old, new in replacements.items():
584
- text = text.replace(old, new)
585
-
586
- text = re.sub(r"[•\-]{2,}", "-", text)
587
-
588
- lines = [line.strip() for line in text.splitlines() if line.strip()]
589
- lines = lines[:5]
590
- text = "\n".join(lines)
591
-
592
- english_words = re.findall(r"[A-Za-z]{4,}", text)
593
- if len(english_words) > 10:
594
- return fallback_customer_service_reply(gender)
595
-
596
- blocked_patterns = [
597
- r"\b\d+\s*جنيه\b",
598
- r"\b\d+\s*egp\b",
599
- r"\bالعنوان\b",
600
- r"\bالمعاد\b",
601
- r"\bالساعة\b",
602
- r"\bالسعر\b",
603
- r"\bالاسعار\b",
604
- r"\bالأسعار\b",
605
- r"\bفرع\s+\w+",
606
- ]
607
-
608
- lowered = normalize_arabic(text)
609
- for pattern in blocked_patterns:
610
- if re.search(pattern, lowered, flags=re.IGNORECASE):
611
- return fallback_customer_service_reply(gender)
612
-
613
- return text
614
-
615
-
616
- # =========================
617
- # Rule-based replies
618
- # =========================
619
-
620
- def generate_rule_based_reply(intent: str, user_text: str, gender: Optional[str] = None) -> str:
621
- subtype = detect_course_subtype(user_text)
622
-
623
- if intent == "GREETING":
624
- greet_line = choose_variant(
625
- gender,
626
- "أهلا بيك في ÄDK.",
627
- "أهلا بيكي في ÄDK.",
628
- "أهلا بيك في ÄDK."
629
- )
630
- ask_line = choose_variant(
631
- gender,
632
- "تحب تبدأ بإيه؟",
633
- "تحبي تبدئي بإيه؟",
634
- "تحب تبدأ بإيه؟"
635
- )
636
- return (
637
- f"{greet_line}\n"
638
- "أقدر أساعدك في أنواع الكورسات، تفاصيل نوع معين، أو معلومات عن المركز.\n"
639
- f"{ask_line}"
640
- )
641
-
642
- if intent == "HUMAN_AGENT":
643
- ask_line = choose_variant(
644
- gender,
645
- "تحب استفسارك يكون عن أونلاين ولا حضور؟",
646
- "تحبي استفسارك يكون عن أونلاين ولا حضور؟",
647
- "تحب استفسارك يكون عن أونلاين ولا حضور؟"
648
- )
649
- return (
650
- "تمام — هسجّل استفسارك لفريق خدمة العملاء عشان يردوا عليك بشكل أدق.\n"
651
- f"{ask_line}"
652
- )
653
-
654
- if intent == "COURSES_MENU":
655
- return build_courses_menu_reply(gender)
656
 
657
- if intent == "CENTER_INFO":
658
- return build_center_info_reply(gender)
659
-
660
- if intent == "ONLINE_COURSES":
661
- return build_course_details_reply("online", gender)
662
-
663
- if intent == "WEEKEND_COURSES":
664
- return build_course_details_reply("weekend", gender)
665
-
666
- if intent == "CHILDREN_COURSES":
667
- return build_course_details_reply("children", gender)
668
-
669
- if intent == "COURSE_TYPE_DETAILS":
670
- if subtype in ["express", "intensive", "regular"]:
671
- return build_course_details_reply(subtype, gender)
672
- return build_courses_menu_reply(gender)
673
-
674
- return ""
675
-
676
-
677
- # =========================
678
- # LLM Helpers
679
- # =========================
680
-
681
- def build_llm_fallback_system_prompt() -> str:
682
- return """
683
- أنت مساعد واتساب رسمي لمركز ÄDK.
684
-
685
- التعليمات:
686
- - رد بالعربية المصرية بشكل مهذب ورسمي وخفيف.
687
- - الرد يكون من 2 إلى 4 سطور فقط.
688
- - لا تضف أي معلومة غير موجودة في المعلومات المتاحة.
689
- - ممنوع ذكر أسعار فعلية أو مواعيد فعلية أو عناوين فعلية أو أرقام فروع.
690
- - لو السؤال خارج المعلومات المتاحة، اقترح بشكل منطقي توجيه الاستفسار لفريق خدمة العملاء.
691
- - لا تذكر أنك نموذج ذكاء اصطناعي.
692
- - لا تكتب مقدمات طويلة.
693
-
694
- المعلومات المتاحة فقط:
695
- 1) المركز:
696
- - ÄDK اتأسس سنة 1998.
697
- - له فروع مختلفة في القاهرة.
698
- - بيقدم كورسات للكبار من A1 لحد C1.
699
- - وبيقدم كورسات للأطفال من A1 لحد B1.
700
- - وهو أول معهد معتمد من Goethe Institute في الشرق الأوسط وشمال أفريقيا.
701
-
702
- 2) أنواع الكورسات:
703
- - Express: Super intensive، 3 محاضرات أسبوعيًا، كل محاضرة 4 ساعات، مدة الـ stage شهرين ونص، وممكن تحجز Stage كاملة.
704
- - Intensive: محاضرتين أسبوعيًا، كل محاضرة 4 ساعات، مدة الـ stage 3 شهور ونص، وممكن تحجز Stage كاملة.
705
- - Regular: تحجز كل level لوحده من A1.1 لحد B1.3، محاضرتين أسبوعيًا، كل محاضرة 4 ساعات، مدة الكورس شهر ونص.
706
- - Weekend: مناسب للمشغولين خلال الأسبوع، وبيتبع نفس outline وساعات الـ Regular.
707
- - Online: متاح بنظام أونلاين.
708
- - Children: من سن 5 لحد 15 سنة، والهدف تنمية مهارات الطفل واللغة وتشجيع التفكير الحر والنقدي والاستقلالية.
709
- """.strip()
710
-
711
-
712
- def process_with_llm(user_text: str, gender: Optional[str] = None) -> str:
713
- if not api_key:
714
- return fallback_customer_service_reply(gender)
715
 
716
- client = openai.OpenAI(api_key=api_key, base_url=base_url)
717
 
 
 
718
  try:
719
- resp = client.chat.completions.create(
720
- model=model_link,
721
  messages=[
722
- {"role": "system", "content": build_llm_fallback_system_prompt()},
723
- {"role": "user", "content": user_text},
724
  ],
725
- max_tokens=120,
726
- temperature=0.1,
727
- top_p=0.4,
728
- stream=False,
729
  )
730
 
731
- answer = resp.choices[0].message.content.strip()
732
- return sanitize_reply(answer, gender)
733
-
734
- except Exception:
735
- return fallback_customer_service_reply(gender)
736
-
737
-
738
- def rewrite_reply_with_llm(reply: str, gender: Optional[str] = None) -> str:
739
- """
740
- LLM participates here only as a rewriter, not as a source of truth.
741
- """
742
- if not api_key or not reply:
743
- return reply
744
-
745
- client = openai.OpenAI(api_key=api_key, base_url=base_url)
746
-
747
- gender_hint = "غير محدد"
748
- if gender == "male":
749
- gender_hint = "مذكر"
750
- elif gender == "female":
751
- gender_hint = "مؤنث"
752
-
753
- prompt = f"""
754
- أعد صياغة الرسالة التالية بالعربية المصرية بشكل طبيعي ولطيف ورسمي قليلًا.
755
 
756
- قواعد مهمة جدًا:
757
- - لا تضف أي معلومة جديدة
758
- - لا تغير المعنى
759
- - لا تضف أسعار أو مواعيد أو عناوين أو أرقام
760
- - حافظ على الرد قصير: من 2 إلى 5 سطور
761
- - لو كانت الصياغة الحالية جيدة، يمكنك إرجاعها كما هي
762
- - حاول مراعاة الجنس اللغوي إن كان واضحًا: {gender_hint}
763
 
764
- الرسالة:
765
- {reply}
766
- """.strip()
767
 
 
 
768
  try:
769
- resp = client.chat.completions.create(
770
- model=model_link,
771
- messages=[
772
- {"role": "system", "content": "You rewrite Arabic business chat replies without adding information."},
773
- {"role": "user", "content": prompt},
774
- ],
775
- max_tokens=140,
776
- temperature=0.2,
777
- top_p=0.5,
778
- stream=False,
779
  )
780
 
781
- enhanced = resp.choices[0].message.content.strip()
782
- return sanitize_reply(enhanced, gender)
783
-
784
- except Exception:
785
- return reply
786
-
787
-
788
- # =========================
789
- # Main Processing
790
- # =========================
791
-
792
- def process_text(user_text: str):
793
- user_text = (user_text or "").strip()
794
- gender = detect_gender(user_text)
795
-
796
- if not user_text:
797
- return {
798
- "ok": True,
799
- "intent": "OTHER",
800
- "gender": gender,
801
- "source": "rule_based",
802
- "reply": "أرسل سؤالك عن الكورسات أو عن المركز، وأنا أساعدك.",
803
- }
804
-
805
- intent = detect_intent(user_text)
806
-
807
- # 1) Known intents -> rule-based
808
- answer = generate_rule_based_reply(intent, user_text, gender)
809
- if answer:
810
- answer = sanitize_reply(answer, gender)
811
- answer = rewrite_reply_with_llm(answer, gender)
812
- answer = sanitize_reply(answer, gender)
813
- return {
814
- "ok": True,
815
- "intent": intent,
816
- "gender": gender,
817
- "source": "rule_based+llm_rewrite",
818
- "reply": answer
819
- }
820
-
821
- # 2) Smart fallback for OTHER
822
- smart_other_answer = generate_other_reply(user_text, gender)
823
- if smart_other_answer:
824
- smart_other_answer = sanitize_reply(smart_other_answer, gender)
825
- smart_other_answer = rewrite_reply_with_llm(smart_other_answer, gender)
826
- smart_other_answer = sanitize_reply(smart_other_answer, gender)
827
- return {
828
- "ok": True,
829
- "intent": intent,
830
- "gender": gender,
831
- "source": "smart_fallback+llm_rewrite",
832
- "reply": smart_other_answer
833
- }
834
-
835
- # 3) Last resort LLM
836
- llm_answer = process_with_llm(user_text, gender)
837
- return {
838
- "ok": True,
839
- "intent": intent,
840
- "gender": gender,
841
- "source": "llm_fallback",
842
- "reply": llm_answer
843
- }
844
-
845
-
846
- # =========================
847
- # API Endpoint
848
- # =========================
849
-
850
- @app.post("/processtext")
851
- async def receive_updates(request: Request):
852
- try:
853
- data = await request.json()
854
- except Exception:
855
  return JSONResponse(
856
- content={"ok": False, "error": "Invalid JSON payload"},
857
- status_code=400
 
 
 
 
858
  )
859
 
860
- print("Received Update:", data)
861
-
862
- result = process_text(data.get("message", ""))
863
- print("Assistant:", result)
864
-
865
- return JSONResponse(content=result)
 
1
  import os
2
+ from typing import List, Literal, Optional
 
3
 
4
+ from fastapi import FastAPI, HTTPException
5
+ from fastapi.responses import JSONResponse
6
+ from openai import OpenAI
7
+ from pydantic import BaseModel, Field
 
8
 
9
+ app = FastAPI(title="GLM-5 Chat API", version="1.0.0")
 
 
10
 
11
+ HF_TOKEN = os.getenv("HF_TOKEN")
12
+ MODEL_NAME = os.getenv("MODEL_NAME", "zai-org/GLM-5")
13
 
14
+ if not HF_TOKEN:
15
+ raise RuntimeError("HF_TOKEN environment variable is missing.")
16
 
17
+ client = OpenAI(
18
+ base_url="https://router.huggingface.co/v1",
19
+ api_key=HF_TOKEN,
20
+ )
21
 
22
 
23
+ class ChatMessage(BaseModel):
24
+ role: Literal["system", "user", "assistant"]
25
+ content: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ class ChatRequest(BaseModel):
29
+ messages: List[ChatMessage] = Field(..., min_length=1)
30
+ temperature: Optional[float] = 0.7
31
+ max_tokens: Optional[int] = 700
32
+ top_p: Optional[float] = 0.95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
 
34
 
35
+ class SimpleChatRequest(BaseModel):
36
+ message: str
37
+ system_prompt: Optional[str] = "أنت مساعد ذكي ومفيد. جاوب بشكل طبيعي وواضح."
38
+ temperature: Optional[float] = 0.7
39
+ max_tokens: Optional[int] = 700
40
+ top_p: Optional[float] = 0.95
41
 
 
 
 
 
 
42
 
43
+ @app.get("/")
44
+ def root():
45
+ return {
46
+ "ok": True,
47
+ "service": "glm5-fastapi",
48
+ "model": MODEL_NAME,
49
+ "endpoints": ["/health", "/chat", "/chat/simple"],
 
 
 
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ @app.get("/health")
54
+ def health():
55
+ return {"ok": True, "model": MODEL_NAME}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
 
57
 
58
+ @app.post("/chat/simple")
59
+ def chat_simple(req: SimpleChatRequest):
60
  try:
61
+ response = client.chat.completions.create(
62
+ model=MODEL_NAME,
63
  messages=[
64
+ {"role": "system", "content": req.system_prompt},
65
+ {"role": "user", "content": req.message},
66
  ],
67
+ temperature=req.temperature,
68
+ max_tokens=req.max_tokens,
69
+ top_p=req.top_p,
 
70
  )
71
 
72
+ reply = response.choices[0].message.content if response.choices else ""
73
+ return JSONResponse(
74
+ {
75
+ "ok": True,
76
+ "model": MODEL_NAME,
77
+ "reply": reply,
78
+ "usage": response.usage.model_dump() if getattr(response, "usage", None) else None,
79
+ }
80
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ except Exception as e:
83
+ raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
84
 
 
 
 
85
 
86
+ @app.post("/chat")
87
+ def chat(req: ChatRequest):
88
  try:
89
+ response = client.chat.completions.create(
90
+ model=MODEL_NAME,
91
+ messages=[m.model_dump() for m in req.messages],
92
+ temperature=req.temperature,
93
+ max_tokens=req.max_tokens,
94
+ top_p=req.top_p,
 
 
 
 
95
  )
96
 
97
+ reply = response.choices[0].message.content if response.choices else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  return JSONResponse(
99
+ {
100
+ "ok": True,
101
+ "model": MODEL_NAME,
102
+ "reply": reply,
103
+ "usage": response.usage.model_dump() if getattr(response, "usage", None) else None,
104
+ }
105
  )
106
 
107
+ except Exception as e:
108
+ raise HTTPException(status_code=500, detail=str(e))