Mr-Help commited on
Commit
7cdd79b
·
verified ·
1 Parent(s): 41c28af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +461 -127
app.py CHANGED
@@ -1,42 +1,38 @@
1
- import openai
2
  import os
3
- from dotenv import load_dotenv, dotenv_values
 
 
 
4
  from fastapi import FastAPI, Request
 
5
  from pydantic import BaseModel
6
- from fastapi.responses import RedirectResponse, HTMLResponse
7
- from fastapi.responses import JSONResponse
8
- import urllib.parse
9
- import requests
10
 
11
- # Load environment variables (assuming your API key is stored in a `.env` file)
 
 
 
12
  load_dotenv()
13
- api_key = os.environ.get('HUGGINGFACEHUB_API_TOKEN')
14
 
15
- # OpenAI API configuration (specific to Meta-Llama-3-8B)
16
  model_link = "meta-llama/Meta-Llama-3-8B-Instruct"
17
  base_url = "https://router.huggingface.co/v1"
18
 
19
  app = FastAPI()
20
 
 
21
  class Message(BaseModel):
22
  message: str
23
 
 
24
  @app.get("/", response_class=HTMLResponse)
25
  async def read_root():
26
- return """Welcome to Up to 12 Chat Processor"""
27
-
28
- @app.post("/processtext")
29
- async def receive_updates(request: Request):
30
- data = await request.json()
31
- print("Received Update:", data)
32
 
33
- result = process_text(data.get("message", ""))
34
- print("Assistant:", result)
35
-
36
- # ضمان إن الرجوع JSON دايمًا
37
- return JSONResponse(content=result)
38
 
39
- import re
 
 
40
 
41
  INTENTS = {
42
  "GREETING": "تحية/بداية محادثة",
@@ -50,140 +46,478 @@ INTENTS = {
50
  "OTHER": "غير معروف/خارج النطاق",
51
  }
52
 
53
- def detect_intent(text: str) -> str:
54
- t = (text or "").strip().lower()
55
 
56
- # 1) Greetings
57
- if re.search(r"\b(hi|hello|hey|start)\b", t) or any(x in t for x in ["اهلا", "أهلا", "السلام", "هاي", "مرحبا", "ابدأ", "ابدء"]):
58
- return "GREETING"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # 2) Human agent
61
- if any(x in t for x in ["خدمة العملاء", "موظف", "حد يرد", "اكلم", "اتواصل", "رقم", "واتساب", "support", "agent"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  return "HUMAN_AGENT"
63
 
64
- # 3) Courses menu / asking about courses generally
65
- if any(x in t for x in ["كورسات", "دورات", "courses", "الكورسات المتاحة", "عايز كورس", "عايز اتعلم", "الالماني عندكم", "الماني"]):
66
- return "COURSES_MENU"
 
 
 
 
 
 
67
 
68
- # 4) Specific course types
69
- # Express / Intensive / Regular / Weekend / Online / Children
70
  if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
71
  return "COURSE_TYPE_DETAILS"
72
- if any(x in t for x in ["intensive", "انتنسب", "مكثف", "مكثفة"]):
 
73
  return "COURSE_TYPE_DETAILS"
74
- if any(x in t for x in ["regular", "ريجولار", "عادي", "منتظم"]):
 
75
  return "COURSE_TYPE_DETAILS"
76
- if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطلة"]):
77
- return "WEEKEND_COURSES"
78
- if any(x in t for x in ["online", "اونلاين", "أونلاين", "زووم", "من البيت"]):
79
- return "ONLINE_COURSES"
80
- if any(x in t for x in ["children", "kids", "اطفال", "أطفال", "طفل", "سن", "سنين"]):
81
- return "CHILDREN_COURSES"
82
 
83
- # 5) Center info
84
- if any(x in t for x in ["المركز", "adk", "ädk", "فروع", "branch", "اتأسس", "تاريخ", "goethe", "معتمد"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  return "CENTER_INFO"
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  return "OTHER"
88
 
89
- KB_TEXT = """
90
- المركز:
91
- (ÄDK)- Egyptian-German Cultural Centre was established in 1998. With various branches in Cairo, ÄDK offers German language courses for adults from A1 to C1 and for children from A1 to B1. Over the years thousands have graduated and have learned in ÄDK. In addition to our language courses, ÄDK organizes cultural projects independently, or in conjunction with the Goethe institute.
92
- ÄDK is the first institute to be accredited from the Goethe institute in the Middle East and North Africa region.
93
-
94
- أنواع الكورسات:
95
- 1) Express Courses:
96
- - Super intensive courses لتعلم الألماني بسرعة وكفاءة. Challenging وتحتاج مجهود.
97
- - ممكن تحجز Stage كاملة (A1-A2-B1).
98
- - 3 محاضرات في الأسبوع، كل محاضرة 4 ساعات.
99
- - مدة الـ stage: شهرين ونصف.
100
-
101
- 2) Intensive Courses:
102
- - ممكن تحجز Stage كاملة (A1-A2-B1).
103
- - محاضرتين في الأسبوع، كل محاضرة 4 ساعات.
104
- - مدة الـ stage: 3 شهور ونصف.
105
-
106
- 3) Regular Courses:
107
- - ممكن تحجز كل level لوحده من (A1.1 إلى B1.3).
108
- - محاضرتين في الأسبوع، كل محاضرة 4 ساعات.
109
- - مدة الكورس: شهر ونصف.
110
-
111
- 4) Weekend Courses:
112
- - للناس اللي مش فاضيين خلال الأسبوع بسبب شغل/دراسة.
113
- - بتتبع نفس outline وساعات الـ Regular (intensive و normal).
114
-
115
- 5) Online Courses:
116
- - attending online.
117
-
118
- 6) Children Courses:
119
- - من 5 سنوات حتى 15 سنة.
120
- - الهدف: تنمية مهارات الطفل واللغة وتشجيع التفكير الحر والنقدي والاستقلالية.
121
- """
122
-
123
- def build_system_prompt(intent: str) -> str:
124
- return f"""
125
- You are the official WhatsApp assistant for ÄDK (Egyptian-German Cultural Centre).
126
-
127
- OUTPUT LANGUAGE:
128
- - Reply in Egyptian Arabic (عامية مصرية) using short, natural sentences.
129
- - Do NOT use formal greetings like رحباً" or "أهلاً". Reply directly to the user's message.
130
-
131
- STYLE RULES (must follow):
132
- - Do NOT produce Arabic grammar mistakes such as "اقترحك" or "يهتم بيك".
133
- Use: "أقترح" / "أنسب" / "تحب" / "ممكن".
134
- - Use the second-person pronoun consistently: use "إنت" (not حضرتك).
135
- - Ask choices correctly: "إنت مهتم بأي نوع كورس؟" / "تحب تعرف تفاصيل أنهي نوع؟"
136
- Never say: "يهتم بيك".
137
- - Max 5 lines. No JSON. No headings. No bullet spam.
138
-
139
- SCOPE RULES:
140
- - Use ONLY the information inside KNOWLEDGE BASE below.
141
- - Never invent prices, schedules, branch addresses, locations, or any details not in KB.
142
- - Never compare competitors or provide general German-course information outside KB.
143
-
144
- OUT-OF-SCOPE HANDLING:
145
- - Do NOT say "المعلومة مش عندي" or apologize at length.
146
- - Instead, use one of these Arabic templates:
147
- 1) "تمام هحوّل سؤالك لفريق خدمة العملاء عشان نديك إجابة دقيقة."
148
- 2) "تمام — هسجّل استفسارك، وخدمة العملاء هيردّوا عليك بالتفاصيل."
149
- 3) "تمام عشان نضمن الدقة، فريق خدمة العملاء هيتواصل معاك ويوضح لك كل التفاصيل."
150
- - Then ask ONE helpful follow-up question (if needed): "تحب أونلاين ولا حضور؟" (do NOT ask for phone number).
151
-
152
- INTENT BEHAVIOR:
153
- - GREETING: send a short welcome + 3 options.
154
- - COURSES_MENU: list available types only (Regular / Intensive / Express / Weekend / Online / Children), then ask which type.
155
- - COURSE_TYPE_DETAILS / ONLINE_COURSES / WEEKEND_COURSES / CHILDREN_COURSES:
156
- answer ONLY from KB, then ask 1 follow-up question.
157
- - CENTER_INFO: answer ONLY from KB, then offer connecting to customer service.
158
-
159
- CURRENT INTENT: {intent}
160
-
161
- KNOWLEDGE BASE:
162
- {KB_TEXT}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  """.strip()
164
 
165
- def process_text(user_text: str):
166
- client = openai.OpenAI(api_key=api_key, base_url=base_url)
167
 
168
- intent = detect_intent(user_text)
169
- system_prompt = build_system_prompt(intent)
 
 
 
170
 
171
  try:
172
  resp = client.chat.completions.create(
173
  model=model_link,
174
  messages=[
175
- {"role": "system", "content": system_prompt},
176
  {"role": "user", "content": user_text},
177
  ],
178
- max_tokens=500,
179
- temperature=0.2,
 
180
  stream=False,
181
  )
182
 
183
  answer = resp.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- # لو تحب ترجع intent كمان (مفيد للـ webhook logic)
 
 
 
186
  return {"ok": True, "intent": intent, "reply": answer}
187
 
188
- except Exception as e:
189
- return {"ok": False, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import re
3
+
4
+ import openai
5
+ from dotenv import load_dotenv
6
  from fastapi import FastAPI, Request
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
  from pydantic import BaseModel
 
 
 
 
9
 
10
+ # =========================
11
+ # Environment / Model Setup
12
+ # =========================
13
+
14
  load_dotenv()
15
+ api_key = os.environ.get("HUGGINGFACEHUB_API_TOKEN")
16
 
17
+ # You can change the model later if needed.
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
+ # =========================
34
+ # Intents
35
+ # =========================
36
 
37
  INTENTS = {
38
  "GREETING": "تحية/بداية محادثة",
 
46
  "OTHER": "غير معروف/خارج النطاق",
47
  }
48
 
 
 
49
 
50
+ # =========================
51
+ # Knowledge Base
52
+ # =========================
53
+
54
+ KB = {
55
+ "center": {
56
+ "name": "ÄDK - Egyptian-German Cultural Centre",
57
+ "established": "1998",
58
+ "branches": "له فروع مختلفة في القاهرة",
59
+ "adults_levels": "للكبار من A1 لحد C1",
60
+ "children_levels": "للأطفال من A1 لحد B1",
61
+ "goethe": "أول معهد معتمد من Goethe Institute في الشرق الأوسط وشمال أفريقيا",
62
+ },
63
+ "courses": {
64
+ "express": {
65
+ "title": "Express Courses",
66
+ "details": [
67
+ "كورسات Super intensive لتعلم الألماني بسرعة وكفاءة",
68
+ "النظام ده Challenging وبيحتاج مجهود",
69
+ "ممكن تحجز Stage كاملة (A1 - A2 - B1)",
70
+ "3 محاضرات في الأسبوع",
71
+ "كل محاضرة 4 ساعات",
72
+ "مدة الـ stage شهرين ونص",
73
+ ],
74
+ },
75
+ "intensive": {
76
+ "title": "Intensive Courses",
77
+ "details": [
78
+ "ممكن تحجز Stage كاملة (A1 - A2 - B1)",
79
+ "محاضرتين في الأسبوع",
80
+ "كل محاضرة 4 ساعات",
81
+ "مدة الـ stage 3 شهور ونص",
82
+ ],
83
+ },
84
+ "regular": {
85
+ "title": "Regular Courses",
86
+ "details": [
87
+ "ممكن تحجز كل level لوحده من A1.1 لحد B1.3",
88
+ "محاضرتين في الأسبوع",
89
+ "كل محاضرة 4 ساعات",
90
+ "مدة الكورس شهر ونص",
91
+ ],
92
+ },
93
+ "weekend": {
94
+ "title": "Weekend Courses",
95
+ "details": [
96
+ "مناسبة للناس اللي مش فاضيين خلال الأسبوع بسبب شغل أو دراسة",
97
+ "بتتبع نفس outline وساعات الـ Regular",
98
+ ],
99
+ },
100
+ "online": {
101
+ "title": "Online Courses",
102
+ "details": [
103
+ "متاحة بنظام أونلاين",
104
+ "الحضور بيكون online",
105
+ ],
106
+ },
107
+ "children": {
108
+ "title": "Children Courses",
109
+ "details": [
110
+ "من سن 5 سنين لحد 15 سنة",
111
+ "الهدف منها تنمية مهارات الطفل واللغة",
112
+ "وكمان تشجيع التفكير الحر والنقدي والاستقلالية",
113
+ ],
114
+ },
115
+ },
116
+ }
117
+
118
+
119
+ # =========================
120
+ # Helpers
121
+ # =========================
122
+
123
+ def normalize_arabic(text: str) -> str:
124
+ if not text:
125
+ return ""
126
+
127
+ text = text.strip().lower()
128
+
129
+ replacements = {
130
+ "أ": "ا",
131
+ "إ": "ا",
132
+ "آ": "ا",
133
+ "ة": "ه",
134
+ "ى": "ي",
135
+ "ؤ": "و",
136
+ "ئ": "ي",
137
+ }
138
 
139
+ for old, new in replacements.items():
140
+ text = text.replace(old, new)
141
+
142
+ return text
143
+
144
+
145
+ def detect_intent(text: str) -> str:
146
+ t = normalize_arabic(text)
147
+
148
+ # 1) Human agent first
149
+ if any(
150
+ x in t
151
+ for x in [
152
+ "خدمه العملاء",
153
+ "خدمة العملاء",
154
+ "موظف",
155
+ "حد يرد",
156
+ "اكلم",
157
+ "اتواصل",
158
+ "رقم",
159
+ "واتساب",
160
+ "support",
161
+ "agent",
162
+ ]
163
+ ):
164
  return "HUMAN_AGENT"
165
 
166
+ # 2) Specific course types first (more precise than general courses menu)
167
+ if any(x in t for x in ["online", "اونلاين", "اون لاين", "zoom", "زووم", "من البيت"]):
168
+ return "ONLINE_COURSES"
169
+
170
+ if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطله", "عطلة"]):
171
+ return "WEEKEND_COURSES"
172
+
173
+ if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سن", "سنين"]):
174
+ return "CHILDREN_COURSES"
175
 
 
 
176
  if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
177
  return "COURSE_TYPE_DETAILS"
178
+
179
+ if any(x in t for x in ["intensive", "مكثف", "مكثفه"]):
180
  return "COURSE_TYPE_DETAILS"
181
+
182
+ if any(x in t for x in ["regular", "ريجولار", "منتظم"]):
183
  return "COURSE_TYPE_DETAILS"
 
 
 
 
 
 
184
 
185
+ # 3) Center info
186
+ if any(
187
+ x in t
188
+ for x in [
189
+ "المركز",
190
+ "adk",
191
+ "ädk",
192
+ "فروع",
193
+ "branch",
194
+ "اتاسس",
195
+ "اتأسس",
196
+ "تاريخ",
197
+ "goethe",
198
+ "معتمد",
199
+ ]
200
+ ):
201
  return "CENTER_INFO"
202
 
203
+ # 4) General courses
204
+ if any(
205
+ x in t
206
+ for x in [
207
+ "كورسات",
208
+ "دورات",
209
+ "courses",
210
+ "الكورسات المتاحه",
211
+ "الكورسات المتاحة",
212
+ "عايز كورس",
213
+ "عايز اتعلم",
214
+ "الماني عندكم",
215
+ "ألماني",
216
+ "الماني",
217
+ ]
218
+ ):
219
+ return "COURSES_MENU"
220
+
221
+ # 5) Greeting
222
+ if re.search(r"\b(hi|hello|hey|start)\b", t) or any(
223
+ x in t for x in ["اهلا", "السلام", "هاي", "مرحبا", "ابدأ", "ابدء", "مسا", "صباح الخير"]
224
+ ):
225
+ return "GREETING"
226
+
227
  return "OTHER"
228
 
229
+
230
+ def detect_course_subtype(text: str) -> str | None:
231
+ t = normalize_arabic(text)
232
+
233
+ if any(x in t for x in ["express", "اكسبريس", "سريع", "super intensive"]):
234
+ return "express"
235
+ if any(x in t for x in ["intensive", "مكثف", "مكثفه"]):
236
+ return "intensive"
237
+ if any(x in t for x in ["regular", "ريجولار", "منتظم"]):
238
+ return "regular"
239
+ if any(x in t for x in ["weekend", "ويكند", "الجمعة", "السبت", "عطله", "عطلة"]):
240
+ return "weekend"
241
+ if any(x in t for x in ["online", "اونلاين", "اون لاين", "zoom", "زووم", "من البيت"]):
242
+ return "online"
243
+ if any(x in t for x in ["children", "kids", "اطفال", "طفل", "سن", "سنين"]):
244
+ return "children"
245
+
246
+ return None
247
+
248
+
249
+ def fallback_customer_service_reply() -> str:
250
+ return (
251
+ "تمام عشان نضمن لك إجابة دقيقة، فريق خدمة العملاء هيقدر يفيدك أكتر.\n"
252
+ "تحب أوجّه استفسارك لهم بخصوص أونلاين ولا حضور؟"
253
+ )
254
+
255
+
256
+ def build_courses_menu_reply() -> str:
257
+ return (
258
+ "عندنا الأنواع دي من كورسات الألماني:\n"
259
+ "Regular / Intensive / Express / Weekend / Online / Children\n"
260
+ "تحب تعرف تفاصيل أنهي نوع؟"
261
+ )
262
+
263
+
264
+ def build_center_info_reply() -> str:
265
+ center = KB["center"]
266
+ return (
267
+ f"ÄDK اتأسس سنة {center['established']}، و{center['branches']}.\n"
268
+ f"الكورسات للكبار {center['adults_levels']}، وللأطفال {center['children_levels']}.\n"
269
+ f"وكمان هو {center['goethe']}.\n"
270
+ "لو عندك سؤال محدد، أقدر أوصله لخدمة العملاء."
271
+ )
272
+
273
+
274
+ def build_course_details_reply(course_key: str) -> str:
275
+ course = KB["courses"][course_key]
276
+ details = course["details"]
277
+
278
+ if course_key == "express":
279
+ return (
280
+ "الـ Express مناسب لو عايز تتعلم بسرعة.\n"
281
+ f"{details[0]}، و{details[1]}.\n"
282
+ f"ممكن تحجز Stage كاملة، والنظام {details[3]}، {details[4]}.\n"
283
+ f"مدة الـ stage {details[5].replace('مدة الـ stage ', '')}.\n"
284
+ "تحب تعرف الفرق بينه وبين الـ Intensive؟"
285
+ )
286
+
287
+ if course_key == "intensive":
288
+ return (
289
+ لـ Intensive ممكن تحجز فيه Stage كاملة من A1 لحد B1.\n"
290
+ f"النظام {details[1]}، و{details[2]}.\n"
291
+ f"مدة الـ stage {details[3].replace('مدة الـ stage ', '')}.\n"
292
+ "تحب أقارن لك بينه وبين الـ Regular؟"
293
+ )
294
+
295
+ if course_key == "regular":
296
+ return (
297
+ "الـ Regular مناسب لو تحب تمشي level level.\n"
298
+ f"ممكن تحجز كل level لوحده من A1.1 لحد B1.3.\n"
299
+ f"النظام {details[1]}، و{details[2]}.\n"
300
+ f"مدة الكورس {details[3].replace('مدة الكورس ', '')}.\n"
301
+ "تحب تعرف لو ده أنسب لك ولا الـ Intensive؟"
302
+ )
303
+
304
+ if course_key == "weekend":
305
+ return (
306
+ "الـ Weekend مناسب لو إنت مشغول خلال الأسبوع بسبب شغل أو دراسة.\n"
307
+ "وبيمشي بنفس outline وساعات الـ Regular.\n"
308
+ "تحب حضور في الويكند ولا كنت بتدور على أونلاين؟"
309
+ )
310
+
311
+ if course_key == "online":
312
+ return (
313
+ "عندنا كورسات أونلاين، والحضور بيكون online.\n"
314
+ "لو مناسب لك النظام ده، أقدر أساعدك تختار النوع الأقرب لاحتياجك.\n"
315
+ "تحب أقولك على الأنواع المتاحة؟"
316
+ )
317
+
318
+ if course_key == "children":
319
+ return (
320
+ "عندنا كورسات أطفال من سن 5 لحد 15 سنة.\n"
321
+ "الهدف منها تنمية مهارات الطفل واللغة.\n"
322
+ "وكمان بتشجع التفكير الحر والنقدي والاستقلالية.\n"
323
+ "تحب أعرفك على نظام الأطفال بشكل أقرب؟"
324
+ )
325
+
326
+ return fallback_customer_service_reply()
327
+
328
+
329
+ def sanitize_reply(text: str) -> str:
330
+ if not text or not text.strip():
331
+ return fallback_customer_service_reply()
332
+
333
+ text = text.strip()
334
+
335
+ replacements = {
336
+ "حضرتك": "إنت",
337
+ "مرحبا": "أهلا",
338
+ "مرحباً": "أهلا",
339
+ "يسعدني": "أقدر",
340
+ "يهتم بيك": "تحب",
341
+ "اقترحك": "أقترح لك",
342
+ "يمكنني": "أقدر",
343
+ "سأقوم": "ه",
344
+ "سوف": "ه",
345
+ "العميل": "إنت",
346
+ "المستخدم": "إنت",
347
+ }
348
+
349
+ for old, new in replacements.items():
350
+ text = text.replace(old, new)
351
+
352
+ # Remove bullets spam
353
+ text = re.sub(r"[•\-]{2,}", "-", text)
354
+
355
+ # Remove extra empty lines
356
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
357
+ lines = lines[:5]
358
+
359
+ text = "\n".join(lines)
360
+
361
+ # Avoid too much English-heavy reply
362
+ english_words = re.findall(r"[A-Za-z]{4,}", text)
363
+ if len(english_words) > 8:
364
+ return fallback_customer_service_reply()
365
+
366
+ # Prevent invented addresses / prices / schedules if model sneaks them in
367
+ blocked_patterns = [
368
+ r"\b\d+\s*جنيه\b",
369
+ r"\b\d+\s*egp\b",
370
+ r"\bالعنوان\b",
371
+ r"\bالمعاد\b",
372
+ r"\bالساعة\b",
373
+ r"\bالسعر\b",
374
+ r"\bالأسعار\b",
375
+ r"\bفرع\s+\w+",
376
+ ]
377
+ lowered = normalize_arabic(text)
378
+ for pattern in blocked_patterns:
379
+ if re.search(pattern, lowered, flags=re.IGNORECASE):
380
+ return fallback_customer_service_reply()
381
+
382
+ return text
383
+
384
+
385
+ def generate_rule_based_reply(intent: str, user_text: str) -> str:
386
+ subtype = detect_course_subtype(user_text)
387
+
388
+ if intent == "GREETING":
389
+ return (
390
+ "أهلا بيك في ÄDK.\n"
391
+ "أقدر أساعدك في أنواع الكورسات، تفاصيل نوع معين، أو معلومات عن المركز.\n"
392
+ "تحب تبدأ بإيه؟"
393
+ )
394
+
395
+ if intent == "HUMAN_AGENT":
396
+ return (
397
+ "تمام — هسجّل استفسارك لفريق خدمة العملاء عشان يردوا عليك بشكل أدق.\n"
398
+ "تحب استفسارك يكون عن أونلاين ولا حضور؟"
399
+ )
400
+
401
+ if intent == "COURSES_MENU":
402
+ return build_courses_menu_reply()
403
+
404
+ if intent == "CENTER_INFO":
405
+ return build_center_info_reply()
406
+
407
+ if intent == "ONLINE_COURSES":
408
+ return build_course_details_reply("online")
409
+
410
+ if intent == "WEEKEND_COURSES":
411
+ return build_course_details_reply("weekend")
412
+
413
+ if intent == "CHILDREN_COURSES":
414
+ return build_course_details_reply("children")
415
+
416
+ if intent == "COURSE_TYPE_DETAILS":
417
+ if subtype in ["express", "intensive", "regular"]:
418
+ return build_course_details_reply(subtype)
419
+ return build_courses_menu_reply()
420
+
421
+ return ""
422
+
423
+
424
+ # =========================
425
+ # LLM Fallback (ONLY for OTHER)
426
+ # =========================
427
+
428
+ def build_system_prompt() -> str:
429
+ return """
430
+ أنت مساعد واتساب رسمي لمركز ÄDK.
431
+ المطلوب:
432
+ - ترد بالعربية المصرية بشكل مهذب ورسمي وخفيف.
433
+ - الرد يكون قصير جدًا: من 2 إلى 4 سطور.
434
+ - ممنوع اختراع أي معلومة غير الموجودة هنا.
435
+ - ممنوع ذكر أسعار أو مواعيد أو عناوين أو أرقام فروع.
436
+ - لو السؤال خارج المعلومات المتاحة، استخدم النص التالي حرفيًا:
437
+ "تمام — عشان نضمن لك إجابة دقيقة، فريق خدمة العملاء هيقدر يفيدك أكتر.
438
+ تحب أوجّه استفسارك لهم بخصوص أونلاين ولا حضور؟"
439
+
440
+ المعلومات المتاحة فقط:
441
+ 1) المركز:
442
+ - ÄDK اتأسس سنة 1998.
443
+ - له فروع مختلفة في القاهرة.
444
+ - بيقدم كورسات للكبار من A1 لحد C1.
445
+ - وبيقدم كورسات للأطفال من A1 لحد B1.
446
+ - وهو أول معهد معتمد من Goethe Institute في الشرق الأوسط وشمال أفريقيا.
447
+
448
+ 2) أنواع الكورسات:
449
+ - Express: Super intensive، 3 محاضرات أسبوعيًا، كل محاضرة 4 ساعات، مدة الـ stage شهرين ونص، وممكن تحجز Stage كاملة.
450
+ - Intensive: محاضرتين أسبوعيًا، كل محاضرة 4 ساعات، مدة الـ stage 3 شهور ونص، وممكن تحجز Stage كاملة.
451
+ - Regular: تحجز كل level لوحده من A1.1 لحد B1.3، محاضرتين أسبوعيًا، كل محاضرة 4 ساعات، مدة الكورس شهر ونص.
452
+ - Weekend: مناسب للمشغولين خلال الأسبوع، وبيتبع نفس outline وساعات الـ Regular.
453
+ - Online: متاح بنظام أونلاين.
454
+ - Children: من سن 5 لحد 15 سنة، والهدف تنمية مهارات الطفل واللغة وتشجيع التفكير الحر والنقدي والاستقلالية.
455
  """.strip()
456
 
 
 
457
 
458
+ def process_with_llm(user_text: str) -> str:
459
+ if not api_key:
460
+ return fallback_customer_service_reply()
461
+
462
+ client = openai.OpenAI(api_key=api_key, base_url=base_url)
463
 
464
  try:
465
  resp = client.chat.completions.create(
466
  model=model_link,
467
  messages=[
468
+ {"role": "system", "content": build_system_prompt()},
469
  {"role": "user", "content": user_text},
470
  ],
471
+ max_tokens=120,
472
+ temperature=0.0,
473
+ top_p=0.3,
474
  stream=False,
475
  )
476
 
477
  answer = resp.choices[0].message.content.strip()
478
+ return sanitize_reply(answer)
479
+
480
+ except Exception:
481
+ return fallback_customer_service_reply()
482
+
483
+
484
+ # =========================
485
+ # Main Processing
486
+ # =========================
487
+
488
+ def process_text(user_text: str):
489
+ user_text = (user_text or "").strip()
490
+
491
+ if not user_text:
492
+ return {
493
+ "ok": True,
494
+ "intent": "OTHER",
495
+ "reply": "أرسل سؤالك عن الكورسات أو عن المركز، وأنا أساعدك.",
496
+ }
497
+
498
+ intent = detect_intent(user_text)
499
 
500
+ # 1) Rule-based first for known intents
501
+ answer = generate_rule_based_reply(intent, user_text)
502
+ if answer:
503
+ answer = sanitize_reply(answer)
504
  return {"ok": True, "intent": intent, "reply": answer}
505
 
506
+ # 2) LLM only for unknown / out-of-scope style messages
507
+ llm_answer = process_with_llm(user_text)
508
+ return {"ok": True, "intent": intent, "reply": llm_answer}
509
+
510
+
511
+ # =========================
512
+ # API Endpoint
513
+ # =========================
514
+
515
+ @app.post("/processtext")
516
+ async def receive_updates(request: Request):
517
+ data = await request.json()
518
+ print("Received Update:", data)
519
+
520
+ result = process_text(data.get("message", ""))
521
+ print("Assistant:", result)
522
+
523
+ return JSONResponse(content=result)