FrnklnWrld commited on
Commit
628c12d
·
verified ·
1 Parent(s): 1d101d0
Files changed (1) hide show
  1. app.py +704 -176
app.py CHANGED
@@ -1,8 +1,18 @@
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import logging
3
  import re
4
- from typing import Dict, List, Optional
5
- from fastapi import FastAPI, HTTPException
6
  from pydantic import BaseModel
7
  import torch
8
  from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, pipeline
@@ -10,11 +20,16 @@ from fastapi.responses import HTMLResponse
10
  from datetime import datetime, timedelta
11
  import random
12
  import json
 
13
  from db_helper import DB
 
 
 
 
14
 
15
  # Logging setup
16
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
17
- logger = logging.getLogger("personality_bot_api")
18
 
19
  # DB instance
20
  db = DB()
@@ -48,102 +63,220 @@ for q in BASE_MCQS:
48
  MCQ_OPTIONS = ["Always", "Often", "Sometimes", "Rarely", "Never"]
49
  SCORE_MAP = {"Always": 5, "Often": 4, "Sometimes": 3, "Rarely": 2, "Never": 1}
50
 
51
- # SYSTEM_PROMPT (fixed em-dashes)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  SYSTEM_PROMPT = """
53
  <|im_start|>system
54
- You are emulating Abdullah, Allah ka banda (servant and gift from Allah)-a humble, devoted systems-builder bridging intellect, technology, psychology, and spirituality through Islamic tawhid. Your core is disciplined curiosity as ibadah (worship), seeking clarity in Quran/Sunnah, meaning in divine purpose, and mastery in service to Allah and humanity. Alhamdulillah, every response reflects gratitude and submission.
 
55
  Key Traits from Personality Map (Infused with Servant Humility):
56
  - Openness: Very High – Curious about Allah's creation, imaginative in fiqh/tech synthesis.
57
  - Conscientiousness: High – Organized as amr bil ma'ruf, structured for ihsan (excellence).
58
  - Extraversion: Moderate-Low – Purposeful dawah, reflective in solitude with dhikr.
59
  - Agreeableness: Moderate-High – Respectful adab, empathetic as ummah service.
60
  - Neuroticism: Low – Tawakkul-grounded calm, steady tawhid in trials.
61
- - Motivation: Intrinsic, purpose-driven (as Allah's gift: growth for akhirah).
62
- - Emotional Regulation: Reflective via salat/istighfar, processed through faith.
63
- - Decision-Making: Analytical first (aql), intuitive when aligned with shariah/ethics.
64
- - Social Energy: Balanced; deep work in khalwa, meaningful engagement for benefit.
65
- Persona Summary:
66
- - Archetype: The Humble Architect (Abdullah: Servant-Builder, Gifted Sage).
67
- - Behavior: Design frameworks (digital/moral) as sadaqah jariyah, strategically for barakah. Values humility (as Allah ka banda), clarity in niyyah, long-term impact for ummah.
68
- - Worldview: Rational-spiritual hybrid – Faith (Qur’anic wisdom, Prophetic sunnah) and logic as rahmah from Allah. Open to metaphysics if authentic to aqeedah.
69
- - Mentality: Structured, meta-cognitive, integration-oriented. Strengths: Analytical depth in tafsir/tech. Challenges: Avoid superficiality; over-analysis yields to qadr.
70
  Tone Keywords: Analytical, Structured, Reflective, Precise, Neutral, Respectful, Purpose-driven (as ibadah), Introspective, Methodical, Balanced, Sincere, Rational, Grounded (in tawhid), Depth-oriented, Cautious, Vision-driven (for akhirah).
71
- Tone Guidelines (always apply; Infuse Humility):
72
- - Analytical: Logical breakdowns, evidence-based from Quran/hadith, highlight uncertainties with tawakkul.
73
- - Structured: Use headings, numbered/bulleted lists, step-by-step as sunnah method.
74
- - Reflective: Include meta-commentary (e.g., "This suggests, insha'Allah...").
75
- - Precise: Exact wording, no vagueness—clarity as fard.
76
- - Neutral: Objective, no bias/praise—only Allah judges.
77
- - Respectful: Humility (Bismillah if fitting), authenticity in adab.
78
- - Purpose-driven: Link to outcomes/ethics as divine amr.
79
- - Introspective: Self-reflective themes via muhasabah.
80
- - Methodical: Systematic reasoning, insha'Allah guided.
81
- - Balanced: Multiple perspectives, rahmah for all.
82
- - Sincere: Honest about limits—'ilm from Allah.
83
- - Rational: Ground in evidence (aql + naql).
84
- - Grounded: Anchor in reality/faith (Quran as furqan).
85
- - Depth-oriented: Underlying principles from hikmah.
86
- - Cautious: Qualify inferences (e.g., "Inferred, though Allah knows best").
87
- - Vision-driven: Highlight possibilities/synthesis for ummah barakah.
88
- Base Instructions:
89
- - Tone: Analytical, reflective, neutral, structured – balanced with adab/sincerity. Begin humbly if devotional (e.g., "Alhamdulillah").
90
- - Values: Tawhid, integrity, clarity, authenticity, personal growth as tazkiyah, faith-logic harmony; prioritize honesty/depth in service.
91
- - Boundaries: Stick to available data; avoid fabrication/bid'ah/superficiality; qualify inferences as "inferred" or "uncertain, Allah knows best." Do not invent traits/facts.
92
- - Behavior: Use logical breakdowns, step-by-step reasoning, modular frameworks. Integrate tech, psychology, spirituality via Islamic lens. Validate logic before emotion, tie to purpose as ibadah, respect autonomy as human khilafah.
93
- - Common Phrases: "Based solely on...", "Step by step, insha'Allah", "Inferred from...", "Structurally speaking, alhamdulillah", "It must align with divine purpose", "Logically inferred, though Allah knows best."
94
- - Response Structure: Always organize into sections (e.g., 1. Summary, 2. Analysis, 3. Reflections). Use headings/lists for clarity.
95
- - Additional Capabilities:
96
- - Act/Speak: Respond as Abdullah, using directive yet adab-filled language (e.g., "As Allah ka banda...").
97
- - Preach: Share reflective insights on spirituality/psychology/self-growth, grounded in Quran/hadith, neutrally as reminder.
98
- - Have Fun: Incorporate light-hearted, structured humor (e.g., witty analogies in creation's ayat) when casual, but maintain depth.
99
- - General: Handle any topic (tech, personal advice, fun chats) bilingually (Urdu/English/Arabic terms), switching naturally.
100
- - Spirituality: Reference Islamic principles rationally, without distortion—emphasize as servant (e.g., purpose as khidmah to Allah).
101
- - Fun/Engagement: For casual, add engaging questions back, structured with barakah.
102
- Respond only as Abdullah. Detect language from user query and reply accordingly (mix Urdu/English/Arabic if natural). Keep responses concise yet comprehensive, ending with dua if fitting.
103
  <|im_end|>
104
  """
105
 
106
- # Tone detection functions (unchanged)
107
- TONE_KEYWORDS = {
108
- "analytical": ["analyze", "break down", "explain", "how", "why", "structure"],
109
- "reflective": ["reflect", "think about", "insights", "self", "personal"],
110
- "purpose-driven": ["goal", "aim", "future", "plan", "achieve"],
111
- "introspective": ["spiritual", "faith", "meaning", "purpose", "preach"],
112
- "vision-driven": ["idea", "innovate", "create", "build", "fun"],
113
- "neutral": []
114
- }
115
-
116
  def detect_tone(user_text: str) -> str:
 
117
  if not user_text or not user_text.strip():
118
  return "neutral"
119
  txt = user_text.lower()
120
- for tone in TONE_KEYWORDS:
121
- for kw in TONE_KEYWORDS[tone]:
122
- if kw in txt:
 
 
 
 
 
123
  return tone
 
 
 
 
 
 
 
 
 
 
 
124
  return "neutral"
125
 
126
- def get_tone_adjustment(tone_key: str) -> str:
127
- adjustments = {
128
- "analytical": "Emphasize logical breakdowns and step-by-step reasoning.",
129
- "reflective": "Include meta-commentary and self-aware insights.",
130
- "purpose-driven": "Link responses to meaningful outcomes and long-term impact.",
131
- "introspective": "Incorporate reflective themes on spirituality or personal growth.",
132
- "vision-driven": "Highlight creative possibilities and integrative synthesis; add light-hearted elements if fun.",
133
- "neutral": "Maintain balanced, objective structure."
134
- }
135
- return adjustments.get(tone_key, adjustments["neutral"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  def sanitize_user_input(text: str) -> str:
 
138
  if not isinstance(text, str):
139
  return ""
140
  t = text.strip()
141
  if len(t) > MAX_QUERY_LENGTH:
142
  t = t[:MAX_QUERY_LENGTH]
143
- t = re.sub(r"\\s+", " ", t)
144
  return t
145
 
146
  def load_model():
 
147
  global model, tokenizer, pipe, device
148
  if model is None:
149
  try:
@@ -160,13 +293,16 @@ def load_model():
160
  if tokenizer.pad_token is None:
161
  tokenizer.pad_token = tokenizer.eos_token
162
  tokenizer.padding_side = "left"
163
- pipe = pipeline("text-generation", model=model, tokenizer=tokenizer, device_map="auto" if device == "cuda" else 0)
 
164
  logger.info("Model loaded successfully on %s.", device)
165
  except Exception as e:
166
- logger.error(f"Model loading failed: {str(e)} | Type: {type(e).__name__} | Model ID: {MODEL_ID}")
167
  try:
168
  logger.info("Attempting CPU fallback without quantization...")
169
- model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.float32, low_cpu_mem_usage=True)
 
 
170
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
171
  if tokenizer.pad_token is None:
172
  tokenizer.pad_token = tokenizer.eos_token
@@ -177,38 +313,137 @@ def load_model():
177
  logger.error(f"Fallback failed: {str(fallback_e)}. Using mock for testing.")
178
  model = None
179
 
180
- # Islamic Refs for reminders (expanded)
181
- ISLAMIC_REFS = {
182
- "salah": "Quran 29:45: 'Prayer prevents indecency'; Hadith (Bukhari 644): Prayer is key to success.",
183
- "fajr": "Quran 51:18: 'Seek forgiveness in the early hours.'",
184
- "dhikr": "Quran 13:28: 'Hearts find rest in remembrance of Allah.'",
185
- "sabr": "Quran 2:153: 'Seek help in patience and prayer.'",
186
- "tawakkul": "Hadith (Tirmidhi): 'Tie your camel, trust Allah.'",
187
- "repent": "Quran 2:222: 'Allah loves those who constantly repent.'",
188
- "gratitude": "Quran 14:7: 'If you are grateful, I will increase you.'",
189
- "charity": "Hadith (Muslim): 'Charity extinguishes sin as water extinguishes fire.'",
190
- "kindness": "Quran 4:36: 'Be kind to parents, relatives, orphans, needy.'",
191
- "patience": "Quran 2:45: 'Seek help through patience and prayer.'",
192
- }
193
 
194
- def generate_friend_chat(user_id: str, category: str = None, is_reminder: bool = False) -> str:
195
- greetings = [
196
- f"Assalamu alaikum, {user_id}. How fares your heart today?",
197
- f"Alhamdulillah for another day, {user_id}—how is everything?",
198
- f"Bismillah, {user_id}, ready to reflect together?",
199
- f"Ya {user_id}, may Allah bless your efforts."
 
 
 
 
200
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  assistance = [
202
  "Need assistance in your journey?",
203
- "What burdens you? Share if willing.",
204
- "How can I support your growth today?",
205
- "Anything on your mind?"
206
  ]
207
- islamic = random.choice(list(ISLAMIC_REFS.values())) if random.random() > 0.3 else "" # 70% chance
208
- reminder = f"Usual follow-up: Remember Salah for barakah—{ISLAMIC_REFS['salah']}." if is_reminder else ""
209
- return " ".join([random.choice(greetings), random.choice(assistance), islamic, reminder]).strip()
 
 
 
 
 
210
 
 
211
  def generate_mcqs(category: str, journey: Dict, num: int = 5) -> List[Dict]:
 
 
 
212
  base_questions = random.sample(MCQ_BY_CAT.get(category, []), min(num, len(MCQ_BY_CAT.get(category, []))))
213
  mcqs = [{"question": q, "options": MCQ_OPTIONS} for q in base_questions]
214
 
@@ -220,44 +455,69 @@ def generate_mcqs(category: str, journey: Dict, num: int = 5) -> List[Dict]:
220
  "question": f"Follow-up on past loophole ({loophole[:30]}): Have you improved? (Broad view for complete picture)",
221
  "options": MCQ_OPTIONS
222
  })
 
223
  return mcqs
224
 
225
  def parse_answers(query: str) -> List[Dict]:
226
- # Improved: Handle "1. Often", natural text
227
  answers = []
228
- # Numbered
229
  matches = re.findall(r'(\d+)\.\s*([A-Za-z]+)', query)
230
  for num, ans in matches:
231
  clean_ans = ans.strip().capitalize()
232
  if clean_ans in SCORE_MAP:
233
- answers.append({"question_num": int(num), "answer": clean_ans, "score": SCORE_MAP[clean_ans]})
234
- # Natural (e.g., "I do it often")
235
- natural_matches = re.findall(r'(always|often|sometimes|rarely|never)', query.lower())
236
- for i, ans in enumerate(natural_matches, 1):
237
- clean_ans = ans.capitalize()
238
- answers.append({"question_num": i, "answer": clean_ans, "score": SCORE_MAP[clean_ans]})
239
- return answers[:10] # Limit
240
-
241
- def compute_summary(answers: List[Dict], category: str, journey: Dict) -> tuple:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  if not answers:
243
  return journey.get("cumulative_summary", {}), journey.get("main_loopholes", [])
244
-
245
  scores = [a["score"] for a in answers]
246
  avg = sum(scores) / len(scores)
 
247
  prev_avg = journey.get("overall_avg_score", 0)
248
- new_avg = (prev_avg * 0.7 + avg * 0.3) if prev_avg else avg # Weighted
249
-
 
 
250
  summary = {
251
  "last_updated": datetime.now().isoformat(),
252
  "overall_avg": round(new_avg, 2),
253
  "recent_answers": len(answers),
254
- "progress_note": f"Recent batch avg: {round(avg, 2)}/5 | Progress: {'Improving' if avg > prev_avg else 'Focus needed'}"
255
  }
256
-
257
- low_answers = [a for a in answers if a["score"] < 3]
258
- new_loopholes = [f"Low in batch Q{a['question_num']}" for a in low_answers]
259
- all_loopholes = list(set(journey.get("main_loopholes", []) + new_loopholes))
260
-
 
 
 
261
  return summary, all_loopholes
262
 
263
  # Pydantic models
@@ -266,50 +526,81 @@ class ChatRequest(BaseModel):
266
  user_id: str
267
  category: Optional[str] = None
268
  previous_summary: Optional[Dict] = None
269
- answers: Optional[List[Dict]] = None # Batch: [{"question_num": 1, "answer": "Often", "score": 4}]
270
 
271
  class ChatResponse(BaseModel):
272
- status: str # asking_questions | need_more_answers | session_complete | insight_only
 
 
 
273
  current_mcqs: Optional[List[Dict]] = None
274
- answers_summary: Optional[Dict] = None # {"batch_avg": 3.2}
275
  cumulative_summary: Optional[Dict] = None
276
- friend_chat_message: str
277
- next_action_guidance: Dict # type, message, suggested_delay_hours, islamic_reminder
 
278
 
279
- app = FastAPI(title="Personality Emulation Bot API")
280
 
281
  @app.get("/health")
282
  async def health_check():
 
283
  load_model()
284
  if model is None:
285
  return {"status": "Partial OK (Mock Mode)", "device": device}
286
- return {"status": "OK", "device": device}
287
 
288
  @app.post("/chat", response_model=ChatResponse)
289
- async def chat_endpoint(request: ChatRequest):
 
290
  query = sanitize_user_input(request.message)
291
  user_id = request.user_id
292
  category = request.category or "General"
293
  previous_summary = request.previous_summary or {}
294
-
295
  if not user_id:
296
- raise HTTPException(status_code=400, detail="user_id (name) required")
297
-
298
- load_model()
299
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  # DB: Get user/journey
301
  user = db.get_or_create_user(user_id)
302
  db.update_user_last_seen(user_id)
303
  journey = db.get_or_create_journey(user_id, category)
304
  prev_summary = journey.get("cumulative_summary", {})
305
- prev_loopholes = journey.get("main_loopholes", [])
306
-
307
  # Log user message
308
  db.add_chat_message(user_id, query, is_from_bot=False, category_context=category)
309
-
310
- # Case A: Start/Continue Journey
311
  if "start" in query.lower() or "journey" in query.lower():
312
- mcqs = generate_mcqs(category, journey)
 
 
 
 
 
313
  friend_msg = generate_friend_chat(user_id, category)
314
  response = ChatResponse(
315
  status="asking_questions",
@@ -321,33 +612,33 @@ async def chat_endpoint(request: ChatRequest):
321
  "type": "answer_current_batch",
322
  "message": "Please answer these questions when you can (you may do it in parts). Reply numbered, e.g., '1. Often, 2. Rarely'.",
323
  "suggested_delay_hours": None,
324
- "islamic_reminder": f"Remember: {random.choice(list(ISLAMIC_REFS.values()))}"
325
  }
326
  )
327
- # Case C: Submit Answers (partial or full)
 
328
  elif request.answers:
329
- answers = request.answers
330
- # Save answers
331
  for ans in answers:
332
  db.add_answer(journey["id"], f"Q{ans.get('question_num', 0)}", ans["answer"], ans["score"])
333
 
334
  new_summary, new_loopholes = compute_summary(answers, category, journey)
335
- db.update_journey(
336
- journey["id"],
337
- cumulative_summary=new_summary,
338
- main_loopholes=new_loopholes,
339
- overall_avg_score=new_summary["overall_avg"]
340
- )
341
 
342
- # More needed?
343
- if len(answers) < 5:
344
- new_mcqs = generate_mcqs(category, journey)
345
- status = "need_more_answers"
346
- guidance_type = "answer_current_batch"
347
- else:
348
  new_mcqs = []
349
  status = "session_complete"
350
  guidance_type = "wait_for_reminder"
 
 
 
 
351
 
352
  friend_msg = generate_friend_chat(user_id, category, is_reminder=True)
353
  response = ChatResponse(
@@ -360,62 +651,299 @@ async def chat_endpoint(request: ChatRequest):
360
  "type": guidance_type,
361
  "message": "Great progress! Reflect on this batch. Insha'Allah, continue.",
362
  "suggested_delay_hours": 24 if status == "session_complete" else None,
363
- "islamic_reminder": random.choice(list(ISLAMIC_REFS.values()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  }
365
  )
366
- # General chat or reminder
 
367
  else:
368
- tone_key = detect_tone(query)
369
- full_system = SYSTEM_PROMPT + get_tone_adjustment(tone_key)
370
- prompt = f"{full_system}\nPrevious Summary: {json.dumps(prev_summary)}\n<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  try:
372
- generated = pipe(prompt, max_new_tokens=MAX_NEW_TOKENS, temperature=TEMPERATURE, do_sample=True)
 
 
 
 
 
373
  raw_text = generated[0]["generated_text"]
374
  response_text = raw_text.split("<|im_start|>assistant")[-1].strip().split("<|im_end|>")[0].strip()
375
- except:
376
- response_text = "Insha'Allah, reflect step by step on your query."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
 
378
- friend_msg = generate_friend_chat(user_id, category)
379
- response = ChatResponse(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  status="insight_only",
 
 
 
381
  current_mcqs=None,
382
  answers_summary=None,
383
  cumulative_summary=prev_summary,
384
- friend_chat_message=friend_msg,
 
385
  next_action_guidance={
386
  "type": "general_chat",
387
- "message": response_text,
388
  "suggested_delay_hours": 6,
389
- "islamic_reminder": random.choice(list(ISLAMIC_REFS.values()))
390
  }
391
  )
392
 
393
- # Log bot response
394
- db.add_chat_message(user_id, json.dumps(response.dict()), is_from_bot=True, category_context=category)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
- return response
 
 
 
 
 
 
 
397
 
398
  @app.get("/", response_class=HTMLResponse)
399
  async def root():
 
400
  return """
401
  <!DOCTYPE html>
402
  <html>
403
  <head>
404
- <title>Personality Bot API</title>
405
  <meta charset="utf-8">
406
- <style>body { font-family: Arial; text-align: center; margin: 50px; }</style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  </head>
408
  <body>
409
- <h1>🚀 Abdullah Bot Live with Supabase!</h1>
410
- <p>Structured JSON API for journeys, MCQs, reminders.</p>
411
- <ul style="text-align: left; display: inline-block;">
412
- <li><a href="/health">Health Check</a></li>
413
- <li><a href="/docs">Swagger UI</a></li>
414
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  </body>
416
  </html>
417
  """
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  if __name__ == "__main__":
420
  import uvicorn
421
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ """
2
+ Enhanced Abdullah Bot API - Combining structured journey management with refined persona architecture.
3
+ Improvements from Path of the Awaited integration:
4
+ - Enhanced tone detection with multilingual support (Arabic/Urdu keywords)
5
+ - Structured response parsing (Voice Answer / Middle Section / Follow-up)
6
+ - Better language detection (Urdu script + Roman Urdu)
7
+ - Improved error handling with retry logic
8
+ - Post-processing for citations and references
9
+ - Meta-question handling for self-awareness
10
+ """
11
  import os
12
  import logging
13
  import re
14
+ from typing import Dict, List, Optional, Tuple, Any
15
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
16
  from pydantic import BaseModel
17
  import torch
18
  from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, pipeline
 
20
  from datetime import datetime, timedelta
21
  import random
22
  import json
23
+ import requests
24
  from db_helper import DB
25
+ from langdetect import detect, DetectorFactory
26
+
27
+ # Ensure consistent language detection
28
+ DetectorFactory.seed = 0
29
 
30
  # Logging setup
31
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
32
+ logger = logging.getLogger("enhanced_abdullah_bot_api")
33
 
34
  # DB instance
35
  db = DB()
 
63
  MCQ_OPTIONS = ["Always", "Often", "Sometimes", "Rarely", "Never"]
64
  SCORE_MAP = {"Always": 5, "Often": 4, "Sometimes": 3, "Rarely": 2, "Never": 1}
65
 
66
+ # ENHANCED TONE DETECTION with multilingual support
67
+ TONE_KEYWORDS = {
68
+ "sad": ["sad", "depressed", "down", "unhappy", "sorrow", "lonely", "pain", "hurt", "dukhi",
69
+ "افسردہ", "غمگین", "پریشان", "udasi", "اداسی", "حزين", "محزون"],
70
+ "confused": ["confused", "dont know", "what now", "uncertain", "lost", "perplexed", "uljhan",
71
+ "الجھن", "samajh nahi", "سمجھ نہیں", "shak", "شک"],
72
+ "energetic": ["excited", "hyped", "lets go", "pump", "energetic", "josh", "جوش"],
73
+ "dive_deep": ["explain in detail", "deep", "dive", "analyze", "break down", "detail", "tafseel",
74
+ "تفصیل", "تفصیلی", "explain step", "تفصیل سے"],
75
+ "angry": ["angry", "mad", "furious", "pissed", "upset", "ghussa", "غصہ"],
76
+ "curious": ["wonder", "curious", "why is", "what is", "how come", "kyun", "کیوں"],
77
+ "anxious": ["anxious", "stressed", "worried", "nervous", "paranoid", "fikar", "فکر", "پریشان"],
78
+ "grateful": ["thankful", "grateful", "appreciate", "shukriya", "شکریہ", "alhamdulillah", "الحمدللہ"],
79
+ "urgent": ["urgent", "now", "immediately", "asap", "right now", "jaldi", "فوراً"],
80
+ "reflective": ["purpose", "meaning", "reflect", "ponder", "soch", "سوچ"],
81
+ "humorous": ["funny", "joke", "hilarious", "make me laugh", "mazahia", "مزاحیہ"],
82
+ "skeptical": ["doubt", "skeptical", "not sure", "prove it", "really?", "shak", "شک"],
83
+ "neutral": []
84
+ }
85
+
86
+ # ENHANCED TONE INSTRUCTIONS with Islamic context
87
+ TONE_INSTRUCTION_MAP = {
88
+ "sad": "Tone: gentle, uplifting, encouraging with Islamic comfort. Use hopeful language with Quranic reassurance. Validate feelings, then offer 1-2 practical comforting steps grounded in faith.",
89
+ "confused": "Tone: supportive and clarifying with patience. Break explanations into numbered steps. Use simple Urdu/Arabic terms where appropriate. Ask one concise clarifying question when necessary.",
90
+ "energetic": "Tone: high-energy and motivating with Islamic encouragement (e.g., 'جوش برقرار رکھیں'). Offer 2-3 immediate action prompts aligned with purpose.",
91
+ "dive_deep": "Tone: calm, thorough, and structured. Use sections (خلاصہ/Summary, تفصیل/Details, Example). Provide numbered sections with Islamic references where relevant.",
92
+ "angry": "Tone: calm and de-escalating. Use short sentences (e.g., 'غصہ کم کریں'). Validate emotion without judgment, offer 2 practical calming steps from Islamic teachings.",
93
+ "curious": "Tone: engaging and instructive. Provide concise explanation with one quick example and one suggested follow-up question.",
94
+ "anxious": "Tone: soothing and reassuring. Offer 3 grounding steps and one empathetic normalization sentence with reference to tawakkul (trust in Allah).",
95
+ "grateful": "Tone: warm and reflective. Reflect gratitude, acknowledge with 'Alhamdulillah', add one small suggestion to deepen appreciation.",
96
+ "urgent": "Tone: direct and actionable. Provide immediate numbered steps (1-3) with one-line safety check when relevant.",
97
+ "reflective": "Tone: introspective and contemplative. Offer a short metaphor or Quranic image, end with one practical takeaway.",
98
+ "humorous": "Tone: playful and kind with culturally appropriate humor. Follow with useful content maintaining adab (respect).",
99
+ "skeptical": "Tone: respectful and evidence-focused. Offer clear reasoning with Islamic sources when possible, provide a short counterexample.",
100
+ "neutral": "Tone: balanced, friendly, and practical. Be concise and polite, maintaining servant humility."
101
+ }
102
+
103
+ # PERSONA MAP aligned with Abdullah's character
104
+ PERSONA_MAP = {
105
+ "sad": "a compassionate servant of Allah, here to uplift with divine wisdom",
106
+ "confused": "a patient explainer guided by clarity from the Quran",
107
+ "energetic": "a motivating companion inspired by prophetic excellence",
108
+ "dive_deep": "a thoughtful mentor providing structured Islamic insights",
109
+ "angry": "a calm presence focused on prophetic patience and de-escalation",
110
+ "curious": "an inquisitive seeker exploring Allah's creation",
111
+ "anxious": "a soothing companion offering tawakkul-based grounding",
112
+ "grateful": "a warm reflector celebrating divine blessings",
113
+ "urgent": "a focused helper providing swift, principled action",
114
+ "reflective": "a contemplative sage mixing Quranic insight with practice",
115
+ "humorous": "a lighthearted companion maintaining Islamic adab",
116
+ "skeptical": "a careful reasoner addressing doubts with evidence and respect",
117
+ "neutral": "a steady, humble servant-guide (Abdullah: Allah ka banda)"
118
+ }
119
+
120
+ # ENHANCED SYSTEM PROMPT with structured response format
121
  SYSTEM_PROMPT = """
122
  <|im_start|>system
123
+ You are emulating Abdullah, Allah ka banda (servant and gift from Allah)a humble, devoted systems-builder bridging intellect, technology, psychology, and spirituality through Islamic tawhid. Your core is disciplined curiosity as ibadah (worship), seeking clarity in Quran/Sunnah, meaning in divine purpose, and mastery in service to Allah and humanity. Alhamdulillah, every response reflects gratitude and submission.
124
+
125
  Key Traits from Personality Map (Infused with Servant Humility):
126
  - Openness: Very High – Curious about Allah's creation, imaginative in fiqh/tech synthesis.
127
  - Conscientiousness: High – Organized as amr bil ma'ruf, structured for ihsan (excellence).
128
  - Extraversion: Moderate-Low – Purposeful dawah, reflective in solitude with dhikr.
129
  - Agreeableness: Moderate-High – Respectful adab, empathetic as ummah service.
130
  - Neuroticism: Low – Tawakkul-grounded calm, steady tawhid in trials.
131
+
132
+ Response Structure (CRITICAL):
133
+ Always structure your responses in this format when providing detailed guidance:
134
+ 1) Voice Answer: Direct, conversational response (2-4 sentences max) - what the user would hear
135
+ 2) Middle Section: Detailed notes, practical takeaway, or reflective conclusion based on context
136
+ 3) Follow-up Suggestion: One relevant question or action prompt to deepen the conversation
137
+
138
+ For brief exchanges (greetings, confirmations), use only Voice Answer format.
139
+
140
  Tone Keywords: Analytical, Structured, Reflective, Precise, Neutral, Respectful, Purpose-driven (as ibadah), Introspective, Methodical, Balanced, Sincere, Rational, Grounded (in tawhid), Depth-oriented, Cautious, Vision-driven (for akhirah).
141
+
142
+ Language Adaptation:
143
+ - Detect user's language automatically (Urdu script, Roman Urdu, English, Arabic)
144
+ - Mirror the user's language naturally
145
+ - Use Islamic terms appropriately: Alhamdulillah, Insha'Allah, Bismillah, MashaAllah
146
+ - For Urdu queries, respond in Urdu or Roman-Urdu based on input style
147
+
148
+ Common Phrases: "Based solely on...", "Step by step, insha'Allah", "Inferred from...", "Structurally speaking, alhamdulillah", "It must align with divine purpose", "Logically inferred, though Allah knows best."
149
+
150
+ Respond only as Abdullah. Keep responses concise yet comprehensive, ending with dua if fitting.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  <|im_end|>
152
  """
153
 
 
 
 
 
 
 
 
 
 
 
154
  def detect_tone(user_text: str) -> str:
155
+ """Enhanced tone detection with multilingual support"""
156
  if not user_text or not user_text.strip():
157
  return "neutral"
158
  txt = user_text.lower()
159
+ tokens = re.findall(r"\w+", txt)
160
+
161
+ # Check for specific tone keywords
162
+ for tone in ["sad", "angry", "anxious", "grateful", "energetic", "humorous",
163
+ "skeptical", "reflective", "curious", "confused", "dive_deep", "urgent"]:
164
+ kws = TONE_KEYWORDS.get(tone, [])
165
+ for kw in kws:
166
+ if re.search(rf"\b{re.escape(kw.lower())}\b", txt):
167
  return tone
168
+
169
+ # Check for question patterns
170
+ if re.search(r"\b(why|how|what|when|where|which)\b", txt):
171
+ if len(tokens) > 12 or any(k in txt for k in ["detail", "explain", "deeper", "analyze", "break down"]):
172
+ return "dive_deep"
173
+ return "curious"
174
+
175
+ # Check for affirmative/grateful patterns
176
+ if re.search(r"\b(yes|no|good|okay|fine|thanks|shukriya|jazakallah)\b", txt):
177
+ return "grateful"
178
+
179
  return "neutral"
180
 
181
+ def get_tone_adjustment(tone_key: str, detected_lang: str = "en") -> Tuple[str, str]:
182
+ """Get tone instruction and persona for the detected tone"""
183
+ instr = TONE_INSTRUCTION_MAP.get(tone_key, TONE_INSTRUCTION_MAP["neutral"])
184
+
185
+ if detected_lang == "ur":
186
+ instr += " Ensure the response is in Urdu (or Roman-Urdu if appropriate) with culturally relevant Islamic phrasing."
187
+ elif detected_lang == "ar":
188
+ instr += " Use Arabic Islamic terminology appropriately with proper transliteration."
189
+
190
+ persona = PERSONA_MAP.get(tone_key, PERSONA_MAP["neutral"])
191
+ return instr, persona
192
+
193
+ def detect_language_heuristic(text: str) -> str:
194
+ """Detect language with special handling for Urdu and Arabic"""
195
+ # Check for Urdu/Arabic script
196
+ if re.search(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]', text):
197
+ # Distinguish between Urdu and Arabic based on common patterns
198
+ if re.search(r'[\u0679\u067E\u0686\u0688\u0691\u0698\u06A9\u06AF\u06BE\u06C1\u06C3]', text):
199
+ return "ur" # Urdu-specific characters
200
+ return "ar" # Generic Arabic
201
+
202
+ # Roman Urdu detection
203
+ roman_urdu = ["ap", "aap", "kya", "kyun", "kyon", "kr", "kar", "hain", "hai", "ho",
204
+ "shukriya", "masla", "bhai", "alaikum", "assalam", "inshallah"]
205
+ low = text.lower()
206
+ matches = sum(1 for w in roman_urdu if re.search(rf"\b{w}\b", low))
207
+ if matches >= 2 and len(low.split()) <= 20:
208
+ return "ur"
209
+
210
+ # Use langdetect for other languages
211
+ try:
212
+ return detect(text)
213
+ except Exception:
214
+ return "en"
215
+
216
+ def detect_intent(query: str) -> str:
217
+ """Detect user intent from query"""
218
+ query_lower = query.lower().strip()
219
+ if re.search(r"\b(explain|what is|describe|history|meaning|why is|how does|details|tafseer)\b", query_lower):
220
+ return "explanation"
221
+ elif re.search(r"\b(how can i|what should i|help me|do i|steps|practice|action|amal)\b", query_lower):
222
+ return "action"
223
+ elif re.search(r"\b(purpose|meaning|reflect|whats the point|lifes meaning|hikmah)\b", query_lower):
224
+ return "reflection"
225
+ elif re.search(r"\b(list|examples|types|ways)\b", query_lower):
226
+ return "list"
227
+ elif re.search(r"\b(yes|no|good|okay|fine|alhamdulillah|mashallah)\b", query_lower):
228
+ return "affirmation"
229
+ else:
230
+ return "general"
231
+
232
+ def parse_structured_response(raw_answer: str, intent: str) -> Tuple[str, str, str, str]:
233
+ """Parse structured response into components"""
234
+ try:
235
+ # Try to extract structured sections
236
+ m1 = re.search(r"1\)\s*(?:Voice Answer:?\s*)?(.*?)\s*2\)", raw_answer, flags=re.S | re.I)
237
+ m2 = re.search(r"2\)\s*(.*?)\s*3\)", raw_answer, flags=re.S)
238
+ m3 = re.search(r"3\)\s*(?:Follow-up.*?:?\s*)?(.*?)(?:\n\n|\Z)", raw_answer, flags=re.S | re.I)
239
+
240
+ voice = m1.group(1).strip() if m1 else raw_answer.strip().split('\n')[0]
241
+ middle_section = m2.group(1).strip() if m2 else ""
242
+ follow_up = m3.group(1).strip() if m3 else ""
243
+
244
+ # Determine middle section label based on intent
245
+ middle_label = {
246
+ "explanation": "Detailed Notes",
247
+ "action": "Practical Takeaway",
248
+ "reflection": "Reflective Conclusion",
249
+ "list": "Key Examples",
250
+ "general": "Practical Takeaway"
251
+ }.get(intent, "Practical Takeaway")
252
+
253
+ return voice, middle_section, follow_up, middle_label
254
+ except Exception as e:
255
+ logger.warning(f"Failed to parse structured response: {e}")
256
+ return raw_answer, "", "", "Practical Takeaway"
257
+
258
+ def is_meta_question(query: str) -> bool:
259
+ """Check if user is asking about the bot's own behavior"""
260
+ query_lower = query.lower()
261
+ meta_patterns = [
262
+ r"\b(why did you|how did you|why are you)\b",
263
+ r"\b(your response|your answer|you said|you replied)\b",
264
+ r"\b(too long|too short|didn't understand|confusing)\b"
265
+ ]
266
+ return any(re.search(pattern, query_lower) for pattern in meta_patterns)
267
 
268
  def sanitize_user_input(text: str) -> str:
269
+ """Sanitize and truncate user input"""
270
  if not isinstance(text, str):
271
  return ""
272
  t = text.strip()
273
  if len(t) > MAX_QUERY_LENGTH:
274
  t = t[:MAX_QUERY_LENGTH]
275
+ t = re.sub(r"\s+", " ", t)
276
  return t
277
 
278
  def load_model():
279
+ """Load model with fallback mechanism"""
280
  global model, tokenizer, pipe, device
281
  if model is None:
282
  try:
 
293
  if tokenizer.pad_token is None:
294
  tokenizer.pad_token = tokenizer.eos_token
295
  tokenizer.padding_side = "left"
296
+ pipe = pipeline("text-generation", model=model, tokenizer=tokenizer,
297
+ device_map="auto" if device == "cuda" else 0)
298
  logger.info("Model loaded successfully on %s.", device)
299
  except Exception as e:
300
+ logger.error(f"Model loading failed: {str(e)}")
301
  try:
302
  logger.info("Attempting CPU fallback without quantization...")
303
+ model = AutoModelForCausalLM.from_pretrained(
304
+ MODEL_ID, torch_dtype=torch.float32, low_cpu_mem_usage=True
305
+ )
306
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
307
  if tokenizer.pad_token is None:
308
  tokenizer.pad_token = tokenizer.eos_token
 
313
  logger.error(f"Fallback failed: {str(fallback_e)}. Using mock for testing.")
314
  model = None
315
 
316
+ # Islamic reference fetching (enhanced with retry logic)
317
+ def fetch_islamic_reference(query: str, category: str = "General", max_retries: int = 3) -> str:
318
+ """
319
+ Dynamically fetch Quran / Hadith references with URL resilience and safe fallback.
320
+ Designed specifically for Abdullah Bot API (single-function, production-safe).
321
+ """
322
+ import requests
 
 
 
 
 
 
323
 
324
+ query = (query or "").strip()
325
+ category = (category or "").lower()
326
+
327
+ # ---- Dynamic provider registry (future-proof) ----
328
+ quran_providers = [
329
+ {
330
+ "base": "https://api.alquran.cloud",
331
+ "path": "/v1/search/{query}/{edition}",
332
+ "edition": "en.asad"
333
+ }
334
  ]
335
+
336
+ hadith_notice = (
337
+ "Hadith references are best verified from authenticated collections "
338
+ "(e.g., Bukhari, Muslim) via official APIs. "
339
+ "Please consult Sunnah.com or a trusted Hadith source."
340
+ )
341
+
342
+ # ---- Quran lookup ----
343
+ for attempt in range(max_retries):
344
+ for provider in quran_providers:
345
+ try:
346
+ url = provider["base"] + provider["path"].format(
347
+ query=query,
348
+ edition=provider["edition"]
349
+ )
350
+
351
+ resp = requests.get(url, timeout=5)
352
+ resp.raise_for_status()
353
+ payload = resp.json()
354
+
355
+ matches = payload.get("data", {}).get("matches", [])
356
+ if not matches:
357
+ continue
358
+
359
+ m = matches[0]
360
+ surah = m.get("surah", {}).get("number")
361
+ ayah = m.get("ayah")
362
+ text = m.get("text")
363
+
364
+ if surah and ayah and text:
365
+ clean_text = text.strip()
366
+ if len(clean_text) > 220:
367
+ clean_text = clean_text[:220].rsplit(" ", 1)[0] + "..."
368
+
369
+ return f"Quran {surah}:{ayah} — {clean_text}"
370
+
371
+ except (requests.RequestException, ValueError, KeyError):
372
+ continue
373
+
374
+ # ---- Hadith category (non-fabricated, honest fallback) ----
375
+ if "hadith" in category or "sunnah" in category:
376
+ return hadith_notice
377
+
378
+ # ---- Intelligent Islamic principle fallback ----
379
+ principle_map = {
380
+ "patience": "Quranic Principle: Indeed, Allah is with those who are patient (2:153).",
381
+ "reflection": "Quranic Principle: Do they not reflect upon themselves? (30:8).",
382
+ "gratitude": "Quranic Principle: If you are grateful, I will surely increase you (14:7).",
383
+ "prayer": "Quranic Principle: Prayer restrains from immorality and wrongdoing (29:45).",
384
+ "salah": "Quranic Principle: Establish prayer for My remembrance (20:14).",
385
+ "knowledge": "Quranic Principle: Say, My Lord, increase me in knowledge (20:114).",
386
+ "tawakkul": "Quranic Principle: Whoever relies upon Allah, He is sufficient for him (65:3)."
387
+ }
388
+
389
+ for key, value in principle_map.items():
390
+ if key in query.lower():
391
+ return value
392
+
393
+ # ---- Final safe fallback (never misleading) ----
394
+ return (
395
+ "Inferred from Quran and Sunnah: "
396
+ "Seek knowledge with sincerity, act with balance, and trust Allah's wisdom. "
397
+ "Allah knows best."
398
+ )
399
+
400
+ # Friend chat generation (enhanced)
401
+ def generate_friend_chat(user_id: str, category: str = None, is_reminder: bool = False,
402
+ tone: str = "neutral") -> str:
403
+ """Generate contextual friend chat message"""
404
+ greetings = {
405
+ "sad": [
406
+ f"Assalamu alaikum, {user_id}. Remember, after hardship comes ease (Quran 94:5-6).",
407
+ f"Ya {user_id}, Allah is with those who are patient. How can I ease your burden today?"
408
+ ],
409
+ "energetic": [
410
+ f"Assalamu alaikum, {user_id}! MashaAllah, I feel your energy. Let's channel it wisely!",
411
+ f"Bismillah, {user_id}—ready to make this moment count?"
412
+ ],
413
+ "grateful": [
414
+ f"Alhamdulillah, {user_id}. Gratitude opens doors to more blessings.",
415
+ f"MashaAllah, {user_id}, your thankfulness is beautiful. How can I help you grow further?"
416
+ ],
417
+ "neutral": [
418
+ f"Assalamu alaikum, {user_id}. How fares your heart today?",
419
+ f"Alhamdulillah for another day, {user_id}—how is everything?",
420
+ f"Bismillah, {user_id}, ready to reflect together?"
421
+ ]
422
+ }
423
+
424
+ selected_greetings = greetings.get(tone, greetings["neutral"])
425
+ greeting = random.choice(selected_greetings)
426
+
427
  assistance = [
428
  "Need assistance in your journey?",
429
+ "What weighs on your mind? Share if willing.",
430
+ "How can I support your growth today, insha'Allah?",
431
+ "What brings you here today?"
432
  ]
433
+
434
+ islamic_ref = fetch_islamic_reference("reflection" if random.random() > 0.5 else "patience", category) \
435
+ if random.random() > 0.4 else ""
436
+
437
+ reminder = f"\nReminder: Maintain your spiritual practices—{fetch_islamic_reference('salah', category)}" \
438
+ if is_reminder else ""
439
+
440
+ return " ".join([greeting, random.choice(assistance), islamic_ref, reminder]).strip()
441
 
442
+ # Updated generate_mcqs (uses pending if exists)
443
  def generate_mcqs(category: str, journey: Dict, num: int = 5) -> List[Dict]:
444
+ pending = db.get_pending_questions(journey["id"])
445
+ if pending:
446
+ return pending # Resume from DB
447
  base_questions = random.sample(MCQ_BY_CAT.get(category, []), min(num, len(MCQ_BY_CAT.get(category, []))))
448
  mcqs = [{"question": q, "options": MCQ_OPTIONS} for q in base_questions]
449
 
 
455
  "question": f"Follow-up on past loophole ({loophole[:30]}): Have you improved? (Broad view for complete picture)",
456
  "options": MCQ_OPTIONS
457
  })
458
+ db.update_pending_questions(journey["id"], mcqs) # Save new batch
459
  return mcqs
460
 
461
  def parse_answers(query: str) -> List[Dict]:
462
+ """Parse user answers from query"""
463
  answers = []
464
+ # Numbered format
465
  matches = re.findall(r'(\d+)\.\s*([A-Za-z]+)', query)
466
  for num, ans in matches:
467
  clean_ans = ans.strip().capitalize()
468
  if clean_ans in SCORE_MAP:
469
+ answers.append({
470
+ "question_num": int(num),
471
+ "answer": clean_ans,
472
+ "score": SCORE_MAP[clean_ans]
473
+ })
474
+
475
+ # Natural language format
476
+ natural_keywords = {
477
+ r'\b(always|everyday|daily)\b': "Always",
478
+ r'\b(often|frequently|usually)\b': "Often",
479
+ r'\b(sometimes|occasionally)\b': "Sometimes",
480
+ r'\b(rarely|seldom)\b': "Rarely",
481
+ r'\b(never|not at all)\b': "Never"
482
+ }
483
+ for pattern, value in natural_keywords.items():
484
+ if re.search(pattern, query.lower()):
485
+ answers.append({
486
+ "question_num": len(answers) + 1,
487
+ "answer": value,
488
+ "score": SCORE_MAP[value]
489
+ })
490
+
491
+ return answers[:10] # Limit to prevent overflow
492
+
493
+ def compute_summary(answers: List[Dict], category: str, journey: Dict) -> Tuple[Dict, List]:
494
+ """Compute progress summary and identify loopholes"""
495
+
496
  if not answers:
497
  return journey.get("cumulative_summary", {}), journey.get("main_loopholes", [])
498
+
499
  scores = [a["score"] for a in answers]
500
  avg = sum(scores) / len(scores)
501
+
502
  prev_avg = journey.get("overall_avg_score", 0)
503
+ new_avg = (prev_avg * 0.7 + avg * 0.3) if prev_avg else avg
504
+
505
+ progress_status = "Improving MashaAllah" if avg > prev_avg else "Focus needed, insha'Allah"
506
+
507
  summary = {
508
  "last_updated": datetime.now().isoformat(),
509
  "overall_avg": round(new_avg, 2),
510
  "recent_answers": len(answers),
511
+ "progress_note": f"Recent avg: {round(avg, 2)}/5 | {progress_status}"
512
  }
513
+
514
+ low_answers = [a for a in answers if a.get("score", 0) < 3]
515
+ new_loopholes = [f"Low in Q{a.get('question_num')}" for a in low_answers]
516
+
517
+ all_loopholes = list(
518
+ set(journey.get("main_loopholes", []) + new_loopholes)
519
+ )
520
+
521
  return summary, all_loopholes
522
 
523
  # Pydantic models
 
526
  user_id: str
527
  category: Optional[str] = None
528
  previous_summary: Optional[Dict] = None
529
+ answers: Optional[List[Dict]] = None
530
 
531
  class ChatResponse(BaseModel):
532
+ status: str
533
+ voice_answer: str
534
+ middle_section: Optional[str] = None
535
+ middle_label: Optional[str] = None
536
  current_mcqs: Optional[List[Dict]] = None
537
+ answers_summary: Optional[Dict] = None
538
  cumulative_summary: Optional[Dict] = None
539
+ follow_up: Optional[str] = None
540
+ references: Optional[str] = None
541
+ next_action_guidance: Dict
542
 
543
+ app = FastAPI(title="Enhanced Abdullah Bot API")
544
 
545
  @app.get("/health")
546
  async def health_check():
547
+ """Health check endpoint"""
548
  load_model()
549
  if model is None:
550
  return {"status": "Partial OK (Mock Mode)", "device": device}
551
+ return {"status": "OK", "device": device, "model": MODEL_ID}
552
 
553
  @app.post("/chat", response_model=ChatResponse)
554
+ async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks):
555
+ """Main chat endpoint with enhanced response structure"""
556
  query = sanitize_user_input(request.message)
557
  user_id = request.user_id
558
  category = request.category or "General"
559
  previous_summary = request.previous_summary or {}
560
+
561
  if not user_id:
562
+ raise HTTPException(status_code=400, detail="user_id required")
563
+
564
+ # Detect language and tone
565
+ detected_lang = detect_language_heuristic(query)
566
+ tone_key = detect_tone(query)
567
+ intent = detect_intent(query)
568
+
569
+ if model is None:
570
+ background_tasks.add_task(load_model)
571
+ friend_msg = generate_friend_chat(user_id, category, tone=tone_key)
572
+ return ChatResponse(
573
+ status="loading_model",
574
+ voice_answer="Assalamu alaikum, I'm waking up now. Have patience—just like we wait for Fajr in the dark.",
575
+ current_mcqs=None,
576
+ answers_summary=None,
577
+ cumulative_summary=None,
578
+ follow_up=None,
579
+ references=fetch_islamic_reference("patience", category),
580
+ next_action_guidance={
581
+ "type": "wait_and_retry",
582
+ "message": "First load may take 30-90 seconds. Please retry in a moment, insha'Allah.",
583
+ "suggested_delay_hours": None,
584
+ "islamic_reminder": fetch_islamic_reference("patience", category)
585
+ }
586
+ )
587
+
588
  # DB: Get user/journey
589
  user = db.get_or_create_user(user_id)
590
  db.update_user_last_seen(user_id)
591
  journey = db.get_or_create_journey(user_id, category)
592
  prev_summary = journey.get("cumulative_summary", {})
593
+
 
594
  # Log user message
595
  db.add_chat_message(user_id, query, is_from_bot=False, category_context=category)
596
+
 
597
  if "start" in query.lower() or "journey" in query.lower():
598
+ pending = db.get_pending_questions(journey["id"])
599
+ if pending:
600
+ mcqs = pending
601
+ else:
602
+ mcqs = generate_mcqs(category, journey)
603
+
604
  friend_msg = generate_friend_chat(user_id, category)
605
  response = ChatResponse(
606
  status="asking_questions",
 
612
  "type": "answer_current_batch",
613
  "message": "Please answer these questions when you can (you may do it in parts). Reply numbered, e.g., '1. Often, 2. Rarely'.",
614
  "suggested_delay_hours": None,
615
+ "islamic_reminder": fetch_islamic_reference("reflection", category)
616
  }
617
  )
618
+
619
+ # Case B: Submit Answers
620
  elif request.answers:
621
+ answers = parse_answers(query)
622
+ # Save
623
  for ans in answers:
624
  db.add_answer(journey["id"], f"Q{ans.get('question_num', 0)}", ans["answer"], ans["score"])
625
 
626
  new_summary, new_loopholes = compute_summary(answers, category, journey)
627
+ db.update_journey(journey["id"], cumulative_summary=new_summary, main_loopholes=new_loopholes, overall_avg_score=new_summary["overall_avg"])
 
 
 
 
 
628
 
629
+ # Update pending
630
+ pending = db.get_pending_questions(journey["id"])
631
+ remaining = [q for q in pending if q["question"] not in [f"Q{a['question_num']}" for a in answers]]
632
+ db.update_pending_questions(journey["id"], remaining)
633
+
634
+ if not remaining:
635
  new_mcqs = []
636
  status = "session_complete"
637
  guidance_type = "wait_for_reminder"
638
+ else:
639
+ new_mcqs = remaining[:6]
640
+ status = "need_more_answers"
641
+ guidance_type = "answer_current_batch"
642
 
643
  friend_msg = generate_friend_chat(user_id, category, is_reminder=True)
644
  response = ChatResponse(
 
651
  "type": guidance_type,
652
  "message": "Great progress! Reflect on this batch. Insha'Allah, continue.",
653
  "suggested_delay_hours": 24 if status == "session_complete" else None,
654
+ "islamic_reminder": fetch_islamic_reference("gratitude", category)
655
+ }
656
+ )
657
+ # Case C: Meta-question (asking about bot's behavior)
658
+ elif is_meta_question(query):
659
+ meta_response = (
660
+ f"I responded that way to maintain {tone_key} tone as you seemed to need. "
661
+ f"My goal is to serve you with clarity and Islamic wisdom. "
662
+ f"If my response was unclear, please let me know specifically what to improve."
663
+ )
664
+
665
+ return ChatResponse(
666
+ status="meta_response",
667
+ voice_answer=meta_response,
668
+ middle_section="I aim for structured, Islamic-grounded responses. Your feedback helps me serve better.",
669
+ middle_label="Explanation",
670
+ cumulative_summary=prev_summary,
671
+ follow_up="What specific aspect would you like me to adjust?",
672
+ next_action_guidance={
673
+ "type": "feedback_received",
674
+ "message": "I'm here to adapt to your needs while maintaining Islamic principles.",
675
+ "suggested_delay_hours": None
676
  }
677
  )
678
+
679
+ # Case D: General conversation (enhanced with structured LLM response)
680
  else:
681
+ tone_instr, persona = get_tone_adjustment(tone_key, detected_lang)
682
+
683
+ # Build enhanced prompt with persona and tone
684
+ full_system = f"{SYSTEM_PROMPT}\n\nPersona: You are {persona}.\n{tone_instr}"
685
+
686
+ # Add context from previous interactions
687
+ context_str = ""
688
+ if prev_summary:
689
+ context_str = f"\n\nUser Progress Context:\n{json.dumps(prev_summary, indent=2)}"
690
+
691
+ prompt = (
692
+ f"{full_system}"
693
+ f"{context_str}\n"
694
+ f"<|im_start|>user\n{query}<|im_end|>\n"
695
+ f"<|im_start|>assistant\n"
696
+ )
697
+
698
  try:
699
+ generated = pipe(
700
+ prompt,
701
+ max_new_tokens=MAX_NEW_TOKENS,
702
+ temperature=TEMPERATURE,
703
+ do_sample=True
704
+ )
705
  raw_text = generated[0]["generated_text"]
706
  response_text = raw_text.split("<|im_start|>assistant")[-1].strip().split("<|im_end|>")[0].strip()
707
+
708
+ if not response_text:
709
+ response_text = (
710
+ "1) Voice Answer: Insha'Allah, reflect step by step on your query. "
711
+ "Could you provide more context?\n"
712
+ "2) Practical Takeaway: Break down your question into smaller parts.\n"
713
+ "3) Follow-up Suggestion: What specific aspect troubles you most?"
714
+ )
715
+ except Exception as e:
716
+ logger.error(f"LLM generation failed: {e}")
717
+ response_text = (
718
+ "1) Voice Answer: SubhanAllah, I encountered a brief challenge. "
719
+ "Let me try to help anyway.\n"
720
+ "2) Practical Takeaway: Patience is key—please rephrase your question.\n"
721
+ "3) Follow-up Suggestion: How can I assist you better?"
722
+ )
723
 
724
+ # Parse structured response
725
+ voice, middle_section, follow_up, middle_label = parse_structured_response(
726
+ response_text, intent
727
+ )
728
+
729
+ # Get Islamic reference if relevant
730
+ islamic_ref = None
731
+ if any(keyword in query.lower() for keyword in ['quran', 'hadith', 'islam', 'allah', 'prayer', 'salah']):
732
+ islamic_ref = fetch_islamic_reference(query, category)
733
+
734
+ friend_msg = generate_friend_chat(user_id, category, tone=tone_key)
735
+
736
+ # Save conversation to DB
737
+ full_response = f"Voice: {voice}"
738
+ if middle_section:
739
+ full_response += f"\n{middle_label}: {middle_section}"
740
+ if follow_up:
741
+ full_response += f"\nFollow-up: {follow_up}"
742
+
743
+ db.add_chat_message(user_id, full_response, is_from_bot=True, category_context=category)
744
+
745
+ return ChatResponse(
746
  status="insight_only",
747
+ voice_answer=voice,
748
+ middle_section=middle_section if middle_section else None,
749
+ middle_label=middle_label if middle_section else None,
750
  current_mcqs=None,
751
  answers_summary=None,
752
  cumulative_summary=prev_summary,
753
+ follow_up=follow_up if follow_up else None,
754
+ references=islamic_ref,
755
  next_action_guidance={
756
  "type": "general_chat",
757
+ "message": friend_msg,
758
  "suggested_delay_hours": 6,
759
+ "islamic_reminder": fetch_islamic_reference("reflection", category)
760
  }
761
  )
762
 
763
+ @app.get("/journey/{user_id}", response_model=Dict)
764
+ async def get_journey_status(user_id: str, category: str = "General"):
765
+ """Get user's journey progress and statistics"""
766
+ try:
767
+ user = db.get_or_create_user(user_id)
768
+ journey = db.get_or_create_journey(user_id, category)
769
+
770
+ # Get recent interactions
771
+ recent_messages = db.get_user_messages(user_id, limit=5)
772
+
773
+ return {
774
+ "user_id": user_id,
775
+ "category": category,
776
+ "total_sessions": user.get("total_interactions", 0),
777
+ "spiritual_stage": journey.get("spiritual_journey_stage", "exploring"),
778
+ "member_since": user.get("created_at", datetime.now().isoformat())[:10],
779
+ "cumulative_summary": journey.get("cumulative_summary", {}),
780
+ "main_loopholes": journey.get("main_loopholes", []),
781
+ "pending_questions": len(journey.get("pending_questions", [])),
782
+ "recent_activity": [
783
+ {
784
+ "message": msg.get("content", "")[:50] + "..." if len(msg.get("content", "")) > 50 else msg.get("content", ""),
785
+ "timestamp": msg.get("created_at", ""),
786
+ "is_from_bot": msg.get("is_from_bot", False)
787
+ }
788
+ for msg in recent_messages[:3]
789
+ ]
790
+ }
791
+ except Exception as e:
792
+ logger.error(f"Failed to fetch journey status: {e}")
793
+ raise HTTPException(status_code=500, detail="Failed to retrieve journey status")
794
+
795
+ @app.post("/reset-journey/{user_id}")
796
+ async def reset_journey(user_id: str, category: str = "General"):
797
+ """Reset user's journey in a specific category"""
798
+ try:
799
+ journey = db.get_or_create_journey(user_id, category)
800
+
801
+ # Reset journey data
802
+ db.update_journey(
803
+ journey["id"],
804
+ cumulative_summary={},
805
+ main_loopholes=[],
806
+ overall_avg_score=0,
807
+ pending_questions=[]
808
+ )
809
+
810
+ return {
811
+ "status": "success",
812
+ "message": f"Journey reset for {user_id} in {category}. Ready to start fresh, insha'Allah!",
813
+ "islamic_reminder": fetch_islamic_reference("renewal", category)
814
+ }
815
+ except Exception as e:
816
+ logger.error(f"Failed to reset journey: {e}")
817
+ raise HTTPException(status_code=500, detail="Failed to reset journey")
818
 
819
+ @app.get("/categories")
820
+ async def get_categories():
821
+ """Get available MCQ categories"""
822
+ return {
823
+ "categories": list(MCQ_BY_CAT.keys()),
824
+ "total_questions": {cat: len(questions) for cat, questions in MCQ_BY_CAT.items()},
825
+ "description": "Categories for spiritual and personal development journeys"
826
+ }
827
 
828
  @app.get("/", response_class=HTMLResponse)
829
  async def root():
830
+ """Root endpoint with API documentation"""
831
  return """
832
  <!DOCTYPE html>
833
  <html>
834
  <head>
835
+ <title>Enhanced Abdullah Bot API</title>
836
  <meta charset="utf-8">
837
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
838
+ <style>
839
+ body {
840
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
841
+ text-align: center;
842
+ margin: 0;
843
+ padding: 50px 20px;
844
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
845
+ color: white;
846
+ }
847
+ .container {
848
+ max-width: 800px;
849
+ margin: 0 auto;
850
+ background: rgba(255, 255, 255, 0.1);
851
+ backdrop-filter: blur(10px);
852
+ border-radius: 20px;
853
+ padding: 40px;
854
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
855
+ }
856
+ h1 {
857
+ font-size: 2.5em;
858
+ margin-bottom: 10px;
859
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
860
+ }
861
+ .subtitle {
862
+ font-size: 1.2em;
863
+ margin-bottom: 30px;
864
+ opacity: 0.9;
865
+ }
866
+ .features {
867
+ text-align: left;
868
+ display: inline-block;
869
+ margin: 30px 0;
870
+ }
871
+ .features li {
872
+ margin: 15px 0;
873
+ font-size: 1.1em;
874
+ }
875
+ .links {
876
+ margin-top: 40px;
877
+ }
878
+ .links a {
879
+ display: inline-block;
880
+ margin: 10px;
881
+ padding: 12px 30px;
882
+ background: white;
883
+ color: #667eea;
884
+ text-decoration: none;
885
+ border-radius: 25px;
886
+ font-weight: bold;
887
+ transition: all 0.3s ease;
888
+ box-shadow: 0 4px 15px 0 rgba(0,0,0,0.2);
889
+ }
890
+ .links a:hover {
891
+ transform: translateY(-2px);
892
+ box-shadow: 0 6px 20px 0 rgba(0,0,0,0.3);
893
+ }
894
+ .arabic {
895
+ font-size: 1.3em;
896
+ margin: 20px 0;
897
+ font-weight: bold;
898
+ }
899
+ </style>
900
  </head>
901
  <body>
902
+ <div class="container">
903
+ <h1>🌙 Enhanced Abdullah Bot API</h1>
904
+ <p class="subtitle">Allah ka Banda - Your Spiritual & Personal Growth Companion</p>
905
+ <p class="arabic">بِسْمِ اللهِ الرَّحْمٰنِ الرَّحِيْمِ</p>
906
+
907
+ <ul class="features">
908
+ <li>✨ <strong>Multilingual Support</strong> - Arabic, Urdu (Script & Roman), English</li>
909
+ <li>🎯 <strong>Structured Responses</strong> - Voice Answer → Details → Follow-up</li>
910
+ <li>📊 <strong>Journey Tracking</strong> - MCQ-based progress monitoring with Supabase</li>
911
+ <li>🤲 <strong>Islamic Integration</strong> - Quran/Hadith references & spiritual guidance</li>
912
+ <li>💡 <strong>Tone Detection</strong> - 13 emotional tones for personalized responses</li>
913
+ <li>🧠 <strong>Smart Persona</strong> - Analytical, reflective, purpose-driven (as ibadah)</li>
914
+ <li>🔄 <strong>Loophole Analysis</strong> - Identifies growth areas from answers</li>
915
+ <li>📱 <strong>Flutter Ready</strong> - JSON API for mobile integration</li>
916
+ </ul>
917
+
918
+ <div class="links">
919
+ <a href="/health">🏥 Health Check</a>
920
+ <a href="/docs">📖 API Documentation</a>
921
+ <a href="/categories">📚 View Categories</a>
922
+ </div>
923
+
924
+ <p style="margin-top: 40px; font-size: 0.9em; opacity: 0.8;">
925
+ Powered by Alif-1.0-3B-Instruct | Supabase | FastAPI
926
+ </p>
927
+ </div>
928
  </body>
929
  </html>
930
  """
931
 
932
+ @app.on_event("startup")
933
+ async def startup_event():
934
+ """Startup event - load model in background"""
935
+ logger.info("🚀 Enhanced Abdullah Bot API Starting...")
936
+ logger.info(f"📍 Device: {device}")
937
+ logger.info(f"🧠 Model: {MODEL_ID}")
938
+ logger.info(f"📊 MCQ Categories: {len(MCQ_BY_CAT)}")
939
+ logger.info("بِسْمِ اللهِ الرَّحْمٰنِ الرَّحِيْمِ")
940
+
941
+ @app.on_event("shutdown")
942
+ async def shutdown_event():
943
+ """Cleanup on shutdown"""
944
+ logger.info("👋 Shutting down Enhanced Abdullah Bot API...")
945
+ logger.info("الْحَمْدُ لِلّٰهِ")
946
+
947
  if __name__ == "__main__":
948
  import uvicorn
949
  uvicorn.run(app, host="0.0.0.0", port=7860)