FrnklnWrld commited on
Commit
fc2ec5e
·
verified ·
1 Parent(s): 8272d69
Files changed (1) hide show
  1. app.py +228 -135
app.py CHANGED
@@ -1,4 +1,68 @@
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)
@@ -22,10 +86,7 @@ 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")
@@ -34,24 +95,16 @@ logger = logging.getLogger("enhanced_abdullah_bot_api")
34
  # DB instance
35
  db = DB()
36
 
37
- # Model configuration
38
- MODEL_ID = "large-traversaal/Alif-1.0-3B-Instruct"
39
- QUANTIZATION_CONFIG = BitsAndBytesConfig(
40
- load_in_4bit=True,
41
- bnb_4bit_compute_dtype=torch.float16,
42
- bnb_4bit_use_double_quant=True,
43
- bnb_4bit_quant_type="nf4"
44
- )
45
  MAX_NEW_TOKENS = 256
46
  TEMPERATURE = 0.7
47
  MAX_QUERY_LENGTH = 1000
48
 
49
- # Lazy load globals
50
- model = None
51
- tokenizer = None
52
- pipe = None
53
- device = "cuda" if torch.cuda.is_available() else "cpu"
54
- logger.info(f"Detected device: {device}")
55
 
56
  # Load MCQs
57
  with open('mcqs.json', 'r') as f:
@@ -275,45 +328,83 @@ def sanitize_user_input(text: str) -> str:
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:
283
- logger.info("Loading model with 4-bit quantization...")
284
- quantization_config = QUANTIZATION_CONFIG if device == "cuda" else None
285
- model = AutoModelForCausalLM.from_pretrained(
286
- MODEL_ID,
287
- quantization_config=quantization_config,
288
- device_map="auto" if device == "cuda" else None,
289
- torch_dtype=torch.float16 if device == "cuda" else torch.float32,
290
- low_cpu_mem_usage=True
291
- )
292
- tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
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
309
- tokenizer.padding_side = "left"
310
- pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
311
- logger.info("Fallback CPU model loaded.")
312
- except Exception as fallback_e:
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.
@@ -440,23 +531,23 @@ def generate_friend_chat(user_id: str, category: str = None, is_reminder: bool =
440
 
441
  return " ".join([greeting, random.choice(assistance), islamic_ref, reminder]).strip()
442
 
443
- # Updated generate_mcqs (uses pending if exists)
444
  def generate_mcqs(category: str, journey: Dict, num: int = 5) -> List[Dict]:
445
- pending = db.get_pending_questions(journey["id"])
446
- if pending:
447
- return pending # Resume from DB
448
- base_questions = random.sample(MCQ_BY_CAT.get(category, []), min(num, len(MCQ_BY_CAT.get(category, []))))
 
449
  mcqs = [{"question": q, "options": MCQ_OPTIONS} for q in base_questions]
450
-
451
  if journey.get("main_loopholes"):
452
  loopholes = journey["main_loopholes"]
453
  for i in range(num - len(mcqs)):
454
  loophole = random.choice(loopholes) if loopholes else "general growth"
455
  mcqs.append({
456
- "question": f"Follow-up on past loophole ({loophole[:30]}): Have you improved? (Broad view for complete picture)",
457
  "options": MCQ_OPTIONS
458
  })
459
- db.update_pending_questions(journey["id"], mcqs) # Save new batch
460
  return mcqs
461
 
462
  def parse_answers(query: str) -> List[Dict]:
@@ -493,32 +584,25 @@ def parse_answers(query: str) -> List[Dict]:
493
 
494
  def compute_summary(answers: List[Dict], category: str, journey: Dict) -> Tuple[Dict, List]:
495
  """Compute progress summary and identify loopholes"""
496
-
497
  if not answers:
498
  return journey.get("cumulative_summary", {}), journey.get("main_loopholes", [])
499
-
500
  scores = [a["score"] for a in answers]
501
  avg = sum(scores) / len(scores)
502
-
503
  prev_avg = journey.get("overall_avg_score", 0)
504
  new_avg = (prev_avg * 0.7 + avg * 0.3) if prev_avg else avg
505
-
506
- progress_status = "Improving MashaAllah" if avg > prev_avg else "Focus needed, insha'Allah"
507
-
508
  summary = {
509
  "last_updated": datetime.now().isoformat(),
510
  "overall_avg": round(new_avg, 2),
511
  "recent_answers": len(answers),
512
- "progress_note": f"Recent avg: {round(avg, 2)}/5 | {progress_status}"
513
  }
514
-
515
- low_answers = [a for a in answers if a.get("score", 0) < 3]
516
- new_loopholes = [f"Low in Q{a.get('question_num')}" for a in low_answers]
517
-
518
- all_loopholes = list(
519
- set(journey.get("main_loopholes", []) + new_loopholes)
520
- )
521
-
522
  return summary, all_loopholes
523
 
524
  # Pydantic models
@@ -546,10 +630,13 @@ app = FastAPI(title="Enhanced Abdullah Bot API")
546
  @app.get("/health")
547
  async def health_check():
548
  """Health check endpoint"""
549
- load_model()
550
- if model is None:
551
- return {"status": "Partial OK (Mock Mode)", "device": device}
552
- return {"status": "OK", "device": device, "model": MODEL_ID}
 
 
 
553
 
554
  @app.post("/chat", response_model=ChatResponse)
555
  async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks):
@@ -567,25 +654,6 @@ async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks)
567
  tone_key = detect_tone(query)
568
  intent = detect_intent(query)
569
 
570
- if model is None:
571
- background_tasks.add_task(load_model)
572
- friend_msg = generate_friend_chat(user_id, category, tone=tone_key)
573
- return ChatResponse(
574
- status="loading_model",
575
- voice_answer="Assalamu alaikum, I'm waking up now. Have patience—just like we wait for Fajr in the dark.",
576
- current_mcqs=None,
577
- answers_summary=None,
578
- cumulative_summary=None,
579
- follow_up=None,
580
- references=fetch_islamic_reference("patience", category),
581
- next_action_guidance={
582
- "type": "wait_and_retry",
583
- "message": "First load may take 30-90 seconds. Please retry in a moment, insha'Allah.",
584
- "suggested_delay_hours": None,
585
- "islamic_reminder": fetch_islamic_reference("patience", category)
586
- }
587
- )
588
-
589
  # DB: Get user/journey
590
  user = db.get_or_create_user(user_id)
591
  db.update_user_last_seen(user_id)
@@ -595,66 +663,99 @@ async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks)
595
  # Log user message
596
  db.add_chat_message(user_id, query, is_from_bot=False, category_context=category)
597
 
 
598
  if "start" in query.lower() or "journey" in query.lower():
599
- pending = db.get_pending_questions(journey["id"])
600
  if pending:
601
  mcqs = pending
602
  else:
603
- mcqs = generate_mcqs(category, journey)
 
 
 
604
 
605
- friend_msg = generate_friend_chat(user_id, category)
606
- response = ChatResponse(
607
  status="asking_questions",
 
608
  current_mcqs=mcqs,
609
  answers_summary=None,
610
  cumulative_summary=prev_summary,
611
- friend_chat_message=friend_msg,
612
  next_action_guidance={
613
  "type": "answer_current_batch",
614
- "message": "Please answer these questions when you can (you may do it in parts). Reply numbered, e.g., '1. Often, 2. Rarely'.",
615
  "suggested_delay_hours": None,
616
  "islamic_reminder": fetch_islamic_reference("reflection", category)
617
  }
618
  )
619
 
620
  # Case B: Submit Answers
621
- elif request.answers:
622
  answers = parse_answers(query)
623
- # Save
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  for ans in answers:
625
- db.add_answer(journey["id"], f"Q{ans.get('question_num', 0)}", ans["answer"], ans["score"])
 
626
 
627
  new_summary, new_loopholes = compute_summary(answers, category, journey)
628
- db.update_journey(journey["id"], cumulative_summary=new_summary, main_loopholes=new_loopholes, overall_avg_score=new_summary["overall_avg"])
 
 
 
 
 
629
 
630
- # Update pending
631
- pending = db.get_pending_questions(journey["id"])
632
- remaining = [q for q in pending if q["question"] not in [f"Q{a['question_num']}" for a in answers]]
633
- db.update_pending_questions(journey["id"], remaining)
634
 
635
  if not remaining:
636
  new_mcqs = []
637
  status = "session_complete"
638
  guidance_type = "wait_for_reminder"
 
639
  else:
640
  new_mcqs = remaining[:6]
641
  status = "need_more_answers"
642
  guidance_type = "answer_current_batch"
 
643
 
644
- friend_msg = generate_friend_chat(user_id, category, is_reminder=True)
645
- response = ChatResponse(
 
646
  status=status,
 
 
 
647
  current_mcqs=new_mcqs,
648
  answers_summary={"batch_avg": round(sum(a["score"] for a in answers) / len(answers), 2)},
649
  cumulative_summary=new_summary,
650
- friend_chat_message=friend_msg,
651
  next_action_guidance={
652
  "type": guidance_type,
653
- "message": "Great progress! Reflect on this batch. Insha'Allah, continue.",
 
654
  "suggested_delay_hours": 24 if status == "session_complete" else None,
655
  "islamic_reminder": fetch_islamic_reference("gratitude", category)
656
  }
657
  )
 
658
  # Case C: Meta-question (asking about bot's behavior)
659
  elif is_meta_question(query):
660
  meta_response = (
@@ -697,24 +798,17 @@ async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks)
697
  )
698
 
699
  try:
700
- generated = pipe(
701
- prompt,
702
- max_new_tokens=MAX_NEW_TOKENS,
703
- temperature=TEMPERATURE,
704
- do_sample=True
705
- )
706
- raw_text = generated[0]["generated_text"]
707
- response_text = raw_text.split("<|im_start|>assistant")[-1].strip().split("<|im_end|>")[0].strip()
708
 
709
- if not response_text:
710
  response_text = (
711
  "1) Voice Answer: Insha'Allah, reflect step by step on your query. "
712
- "Could you provide more context?\n"
713
  "2) Practical Takeaway: Break down your question into smaller parts.\n"
714
  "3) Follow-up Suggestion: What specific aspect troubles you most?"
715
  )
716
  except Exception as e:
717
- logger.error(f"LLM generation failed: {e}")
718
  response_text = (
719
  "1) Voice Answer: SubhanAllah, I encountered a brief challenge. "
720
  "Let me try to help anyway.\n"
@@ -932,10 +1026,9 @@ async def root():
932
 
933
  @app.on_event("startup")
934
  async def startup_event():
935
- """Startup event - load model in background"""
936
- logger.info("🚀 Enhanced Abdullah Bot API Starting...")
937
- logger.info(f"📍 Device: {device}")
938
- logger.info(f"🧠 Model: {MODEL_ID}")
939
  logger.info(f"📊 MCQ Categories: {len(MCQ_BY_CAT)}")
940
  logger.info("بِسْمِ اللهِ الرَّحْمٰنِ الرَّحِيْمِ")
941
 
 
1
+ def detect_language_heuristic(text: str) -> str:
2
+ """
3
+ Detect language using regex patterns (no external dependencies).
4
+ Supports: Urdu (ur), Arabic (ar), English (en)
5
+ """
6
+ if not text or not text.strip():
7
+ return "en"
8
+
9
+ # Check for Urdu/Arabic script (Unicode ranges)
10
+ # Arabic: U+0600–U+06FF, U+0750–U+077F, U+08A0–U+08FF
11
+ # Urdu uses Arabic script with additional characters
12
+ if re.search(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]', text):
13
+ # Distinguish between Urdu and Arabic based on specific characters
14
+ # Urdu-specific: ٹ پ ڈ ڑ ژ ک گ ھ ہ ے
15
+ urdu_chars = r'[\u0679\u067E\u0688\u0691\u0698\u06A9\u06AF\u06BE\u06C1\u06D2]'
16
+ if re.search(urdu_chars, text):
17
+ return "ur"
18
+ # Check for common Urdu words in Arabic script
19
+ urdu_words = ['ہے', 'کا', 'کی', 'کے', 'میں', 'نے', 'سے', 'کو', 'اور', 'یہ']
20
+ if any(word in text for word in urdu_words):
21
+ return "ur"
22
+ return "ar" # Default to Arabic for generic Arabic script
23
+
24
+ # Roman Urdu detection (transliterated Urdu using Latin script)
25
+ roman_urdu_words = [
26
+ "ap", "aap", "kya", "kyun", "kyon", "kr", "kar", "karo", "kare",
27
+ "hain", "hai", "ho", "hun", "hoon", "tha", "thi", "the",
28
+ "shukriya", "masla", "bhai", "bhaiyya", "yaar", "dost",
29
+ "alaikum", "assalam", "walaikum", "salaam",
30
+ "inshallah", "mashallah", "subhanallah", "alhamdulillah",
31
+ "nahi", "nahin", "kuch", "kuch", "kal", "aj", "abhi",
32
+ "mein", "main", "ne", "ko", "se", "ka", "ki", "ke",
33
+ "acha", "thik", "bilkul", "zaroor", "shayad"
34
+ ]
35
+
36
+ text_lower = text.lower()
37
+ words = re.findall(r'\b\w+\b', text_lower)
38
+
39
+ # Count Roman Urdu word matches
40
+ urdu_matches = sum(1 for word in words if word in roman_urdu_words)
41
+
42
+ # If we have significant Roman Urdu matches (at least 20% of words or 2+ matches)
43
+ if len(words) > 0:
44
+ match_ratio = urdu_matches / len(words)
45
+ if urdu_matches >= 2 or (match_ratio >= 0.2 and len(words) <= 20):
46
+ return "ur"
47
+
48
+ # Default to English
49
+ return "en" "yaar", "dost",
50
+ "alaikum", "assalam", "walaikum", "salaam",
51
+ "inshallah", "mashallah", "subhanallah", "alhamdulillah",
52
+ "nahi", "nahin", "kuch", "kuch", "kal", "aj", "abhi",
53
+ "mein", "main", "ne", "ko", "se", "ka", "ki", "ke",
54
+ "acha", "thik", "bilkul", "zaroor", "shayad"
55
+ ]
56
+
57
+ text_lower = text.lower()
58
+ words = re.findall(r'\b\w+\b', text_lower)
59
+
60
+ # Count Roman Urdu word matches
61
+ urdu_matches = sum(1 for word in words if word in roman_urdu_words)
62
+
63
+ # If we have significant Roman Urdu matches (at least 20% of words or 2+ matches)
64
+ if len(words) > 0:
65
+ match"""
66
  Enhanced Abdullah Bot API - Combining structured journey management with refined persona architecture.
67
  Improvements from Path of the Awaited integration:
68
  - Enhanced tone detection with multilingual support (Arabic/Urdu keywords)
 
86
  import json
87
  import requests
88
  from db_helper import DB
89
+ # No external language detection library needed - using regex-based detection
 
 
 
90
 
91
  # Logging setup
92
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
 
95
  # DB instance
96
  db = DB()
97
 
98
+ # HF Inference API Configuration (FREE TIER COMPATIBLE - no local model)
99
+ # Using non-gated model for instant access
100
+ HF_API_URL = "https://api-inference.huggingface.co/models/Qwen/Qwen2-1.5B-Instruct"
101
+ HF_TOKEN = os.getenv("HF_TOKEN", "") # Optional for public models
 
 
 
 
102
  MAX_NEW_TOKENS = 256
103
  TEMPERATURE = 0.7
104
  MAX_QUERY_LENGTH = 1000
105
 
106
+ # No local model loading needed - using HF serverless API
107
+ logger.info("Using HF Inference API (Free Tier Compatible)")
 
 
 
 
108
 
109
  # Load MCQs
110
  with open('mcqs.json', 'r') as f:
 
328
  t = re.sub(r"\s+", " ", t)
329
  return t
330
 
331
+ def query_hf_inference_api(prompt: str, max_retries: int = 3) -> str:
332
+ """
333
+ Query HuggingFace Inference API (Free Tier Compatible)
334
+ No local model loading required
335
+ Works with public models (no token needed) or private models (with token)
336
+ """
337
+ headers = {}
338
+ if HF_TOKEN: # Only add auth header if token exists
339
+ headers["Authorization"] = f"Bearer {HF_TOKEN}"
340
+
341
+ payload = {
342
+ "inputs": prompt,
343
+ "parameters": {
344
+ "max_new_tokens": MAX_NEW_TOKENS,
345
+ "temperature": TEMPERATURE,
346
+ "do_sample": True,
347
+ "return_full_text": False
348
+ }
349
+ }
350
+
351
+ for attempt in range(max_retries):
352
  try:
353
+ response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=60)
354
+
355
+ # Handle model loading (first request after sleep)
356
+ if response.status_code == 503:
357
+ error_data = response.json()
358
+ if "loading" in str(error_data).lower():
359
+ estimated_time = error_data.get("estimated_time", 20)
360
+ logger.info(f"Model loading, waiting {estimated_time}s...")
361
+ if attempt < max_retries - 1:
362
+ import time
363
+ time.sleep(min(estimated_time, 30)) # Cap at 30s
364
+ continue
365
+ return "Model is waking up. Please retry in 20 seconds, insha'Allah."
366
+
367
+ # Handle gated model error
368
+ if response.status_code == 401 or response.status_code == 403:
369
+ logger.error("Model access denied - check if model is gated or token is valid")
370
+ return "Service requires authentication. Please contact administrator."
371
+
372
+ response.raise_for_status()
373
+ result = response.json()
374
+
375
+ # Handle different response formats
376
+ if isinstance(result, list) and len(result) > 0:
377
+ generated = result[0].get("generated_text", "")
378
+ # Clean up response (remove prompt echo if present)
379
+ if "<|im_start|>assistant" in generated:
380
+ generated = generated.split("<|im_start|>assistant")[-1].split("<|im_end|>")[0]
381
+ return generated.strip()
382
+ elif isinstance(result, dict):
383
+ generated = result.get("generated_text", "")
384
+ if "<|im_start|>assistant" in generated:
385
+ generated = generated.split("<|im_start|>assistant")[-1].split("<|im_end|>")[0]
386
+ return generated.strip()
387
+
388
+ return ""
389
+
390
+ except requests.Timeout:
391
+ logger.warning(f"HF API timeout on attempt {attempt + 1}")
392
+ if attempt < max_retries - 1:
393
+ import time
394
+ time.sleep(2)
395
+ continue
396
+ return "Response timeout. Please try again, insha'Allah."
397
  except Exception as e:
398
+ logger.error(f"HF API error on attempt {attempt + 1}: {e}")
399
+ if attempt < max_retries - 1:
400
+ import time
401
+ time.sleep(2)
402
+ continue
403
+ return f"Service temporarily unavailable. Please try again."
404
+
405
+ return "Unable to generate response. Please try again."
406
+
407
+ # Islamic reference fetching (production-ready with robust fallback)
 
 
 
 
 
 
 
408
  def fetch_islamic_reference(query: str, category: str = "General", max_retries: int = 3) -> str:
409
  """
410
  Dynamically fetch Quran / Hadith references with URL resilience and safe fallback.
 
531
 
532
  return " ".join([greeting, random.choice(assistance), islamic_ref, reminder]).strip()
533
 
534
+ # MCQ generation (unchanged)
535
  def generate_mcqs(category: str, journey: Dict, num: int = 5) -> List[Dict]:
536
+ """Generate MCQs from base set and loopholes"""
537
+ base_questions = random.sample(
538
+ MCQ_BY_CAT.get(category, []),
539
+ min(num, len(MCQ_BY_CAT.get(category, [])))
540
+ )
541
  mcqs = [{"question": q, "options": MCQ_OPTIONS} for q in base_questions]
542
+
543
  if journey.get("main_loopholes"):
544
  loopholes = journey["main_loopholes"]
545
  for i in range(num - len(mcqs)):
546
  loophole = random.choice(loopholes) if loopholes else "general growth"
547
  mcqs.append({
548
+ "question": f"Follow-up on past area ({loophole[:30]}): Have you improved?",
549
  "options": MCQ_OPTIONS
550
  })
 
551
  return mcqs
552
 
553
  def parse_answers(query: str) -> List[Dict]:
 
584
 
585
  def compute_summary(answers: List[Dict], category: str, journey: Dict) -> Tuple[Dict, List]:
586
  """Compute progress summary and identify loopholes"""
 
587
  if not answers:
588
  return journey.get("cumulative_summary", {}), journey.get("main_loopholes", [])
589
+
590
  scores = [a["score"] for a in answers]
591
  avg = sum(scores) / len(scores)
 
592
  prev_avg = journey.get("overall_avg_score", 0)
593
  new_avg = (prev_avg * 0.7 + avg * 0.3) if prev_avg else avg
594
+
 
 
595
  summary = {
596
  "last_updated": datetime.now().isoformat(),
597
  "overall_avg": round(new_avg, 2),
598
  "recent_answers": len(answers),
599
+ "progress_note": f"Recent avg: {round(avg, 2)}/5 | {'Improving MashaAllah' if avg > prev_avg else 'Focus needed, insha'Allah'}"
600
  }
601
+
602
+ low_answers = [a for a in answers if a["score"] < 3]
603
+ new_loopholes = [f"Low in Q{a['question_num']}" for a in low_answers]
604
+ all_loopholes = list(set(journey.get("main_loopholes", []) + new_loopholes))
605
+
 
 
 
606
  return summary, all_loopholes
607
 
608
  # Pydantic models
 
630
  @app.get("/health")
631
  async def health_check():
632
  """Health check endpoint"""
633
+ return {
634
+ "status": "OK",
635
+ "mode": "HF Inference API (Free Tier)",
636
+ "model": "Qwen/Qwen2-1.5B-Instruct (public, non-gated)",
637
+ "api_available": True,
638
+ "token_configured": bool(HF_TOKEN)
639
+ }
640
 
641
  @app.post("/chat", response_model=ChatResponse)
642
  async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks):
 
654
  tone_key = detect_tone(query)
655
  intent = detect_intent(query)
656
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  # DB: Get user/journey
658
  user = db.get_or_create_user(user_id)
659
  db.update_user_last_seen(user_id)
 
663
  # Log user message
664
  db.add_chat_message(user_id, query, is_from_bot=False, category_context=category)
665
 
666
+ # Case A: Start/Continue Journey
667
  if "start" in query.lower() or "journey" in query.lower():
668
+ pending = journey.get("pending_questions", [])
669
  if pending:
670
  mcqs = pending
671
  else:
672
+ mcqs = generate_mcqs(category, journey, num=6)
673
+ db.update_journey(journey["id"], pending_questions=mcqs)
674
+
675
+ friend_msg = generate_friend_chat(user_id, category, tone=tone_key)
676
 
677
+ return ChatResponse(
 
678
  status="asking_questions",
679
+ voice_answer=friend_msg,
680
  current_mcqs=mcqs,
681
  answers_summary=None,
682
  cumulative_summary=prev_summary,
683
+ follow_up="Take your time to reflect on each question. Answer when ready, numbered format works best.",
684
  next_action_guidance={
685
  "type": "answer_current_batch",
686
+ "message": "Reply with numbered answers, e.g., '1. Often, 2. Rarely'",
687
  "suggested_delay_hours": None,
688
  "islamic_reminder": fetch_islamic_reference("reflection", category)
689
  }
690
  )
691
 
692
  # Case B: Submit Answers
693
+ elif request.answers or any(re.search(r'\d+\.\s*[A-Za-z]+', query) for _ in [1]):
694
  answers = parse_answers(query)
695
+
696
+ if not answers:
697
+ return ChatResponse(
698
+ status="need_clarification",
699
+ voice_answer="I didn't catch your answers clearly. Please use numbered format: '1. Often, 2. Sometimes'",
700
+ current_mcqs=journey.get("pending_questions", [])[:6],
701
+ cumulative_summary=prev_summary,
702
+ follow_up="Would you like me to resend the questions?",
703
+ next_action_guidance={
704
+ "type": "answer_current_batch",
705
+ "message": "Use clear numbered format for answers",
706
+ "suggested_delay_hours": None
707
+ }
708
+ )
709
+
710
+ # Save answers
711
  for ans in answers:
712
+ db.add_answer(journey["id"], f"Q{ans.get('question_num', 0)}",
713
+ ans["answer"], ans["score"])
714
 
715
  new_summary, new_loopholes = compute_summary(answers, category, journey)
716
+ db.update_journey(
717
+ journey["id"],
718
+ cumulative_summary=new_summary,
719
+ main_loopholes=new_loopholes,
720
+ overall_avg_score=new_summary["overall_avg"]
721
+ )
722
 
723
+ # Update pending questions
724
+ remaining = [q for q in journey.get("pending_questions", [])
725
+ if q["question"] not in [f"Q{a['question_num']}" for a in answers]]
726
+ db.update_journey(journey["id"], pending_questions=remaining)
727
 
728
  if not remaining:
729
  new_mcqs = []
730
  status = "session_complete"
731
  guidance_type = "wait_for_reminder"
732
+ voice_msg = f"Alhamdulillah, {user_id}! Session complete. Your progress: {new_summary['progress_note']}"
733
  else:
734
  new_mcqs = remaining[:6]
735
  status = "need_more_answers"
736
  guidance_type = "answer_current_batch"
737
+ voice_msg = f"JazakAllah khair, {user_id}. {len(answers)} answers recorded. Continue when ready."
738
 
739
+ friend_msg = generate_friend_chat(user_id, category, is_reminder=True, tone=tone_key)
740
+
741
+ return ChatResponse(
742
  status=status,
743
+ voice_answer=voice_msg,
744
+ middle_section=new_summary.get('progress_note'),
745
+ middle_label="Progress Summary",
746
  current_mcqs=new_mcqs,
747
  answers_summary={"batch_avg": round(sum(a["score"] for a in answers) / len(answers), 2)},
748
  cumulative_summary=new_summary,
749
+ follow_up=friend_msg if status == "session_complete" else "Continue with remaining questions when ready.",
750
  next_action_guidance={
751
  "type": guidance_type,
752
+ "message": "Great progress! Reflect on these insights, insha'Allah." if status == "session_complete"
753
+ else "Answer remaining questions at your pace.",
754
  "suggested_delay_hours": 24 if status == "session_complete" else None,
755
  "islamic_reminder": fetch_islamic_reference("gratitude", category)
756
  }
757
  )
758
+
759
  # Case C: Meta-question (asking about bot's behavior)
760
  elif is_meta_question(query):
761
  meta_response = (
 
798
  )
799
 
800
  try:
801
+ response_text = query_hf_inference_api(prompt)
 
 
 
 
 
 
 
802
 
803
+ if not response_text or "Model is waking up" in response_text:
804
  response_text = (
805
  "1) Voice Answer: Insha'Allah, reflect step by step on your query. "
806
+ "The service is warming up. Could you provide more context?\n"
807
  "2) Practical Takeaway: Break down your question into smaller parts.\n"
808
  "3) Follow-up Suggestion: What specific aspect troubles you most?"
809
  )
810
  except Exception as e:
811
+ logger.error(f"HF API generation failed: {e}")
812
  response_text = (
813
  "1) Voice Answer: SubhanAllah, I encountered a brief challenge. "
814
  "Let me try to help anyway.\n"
 
1026
 
1027
  @app.on_event("startup")
1028
  async def startup_event():
1029
+ """Startup event - no model loading needed for free tier"""
1030
+ logger.info("🚀 Enhanced Abdullah Bot API Starting (Free Tier Mode)...")
1031
+ logger.info("📡 Using HF Inference API - No local models")
 
1032
  logger.info(f"📊 MCQ Categories: {len(MCQ_BY_CAT)}")
1033
  logger.info("بِسْمِ اللهِ الرَّحْمٰنِ الرَّحِيْمِ")
1034