ragV98 commited on
Commit
8d4ff93
·
1 Parent(s): bbce544

initiated mistral and integrated classifier, cross fingers

Browse files
components/LLMs/Classifier.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/LLMs/Classifier.py
2
+ # Zero-shot intent classifier using Together AI (Mistral-7B-Instruct)
3
+
4
+ from typing import Dict, Literal, Tuple
5
+ import json
6
+ import re
7
+
8
+ from components.LLMs.Mistral import MistralTogetherClient, build_messages
9
+
10
+ # ---- Define your canonical intents (keep this list stable) ----
11
+ Intent = Literal[
12
+ "headlines_request", # user wants today's headlines/digest/news
13
+ "preferences_update", # change topics, regions, time, etc.
14
+ "greeting", # hi/hello/hey
15
+ "help", # how to use / what can you do
16
+ "small_talk", # chitchat, jokes, idle talk
17
+ "chat_question", # general Q&A about news/economy/etc.
18
+ "unsubscribe", # stop/opt-out
19
+ "other" # anything else
20
+ ]
21
+
22
+ INTENT_SET = [
23
+ "headlines_request",
24
+ "preferences_update",
25
+ "greeting",
26
+ "help",
27
+ "small_talk",
28
+ "chat_question",
29
+ "unsubscribe",
30
+ "other",
31
+ ]
32
+
33
+ # Optional: synonyms that you may normalize before passing to downstream logic
34
+ TRIGGER_SYNONYMS = {
35
+ "headlines_request": ["headlines", "digest", "daily", "news", "today's headlines", "view headlines"],
36
+ "unsubscribe": ["stop", "opt out", "unsubscribe", "cancel"],
37
+ "help": ["help", "how it works", "what can you do"],
38
+ "greeting": ["hi", "hello", "hey"],
39
+ }
40
+
41
+ SYSTEM_PROMPT = (
42
+ "You are an intent classifier for a WhatsApp news assistant called NuseAI. "
43
+ "Choose exactly one label from the allowed list, and produce STRICT JSON: "
44
+ '{"label": "<one_of_allowed>", "confidence": <0..1>, "reason": "<short>"}.\n\n'
45
+ f"ALLOWED_LABELS = {INTENT_SET}.\n"
46
+ "- headlines_request: user asks for today's headlines, digest, news, or to view/open the feed.\n"
47
+ "- preferences_update: user wants to set or change interests, topics, regions, or delivery time.\n"
48
+ "- greeting: greetings like hi/hello/hey.\n"
49
+ "- help: asks how to use the bot, commands, or capabilities.\n"
50
+ "- small_talk: casual chat or jokes, not seeking info.\n"
51
+ "- chat_question: general info or Q&A unrelated to getting today's digest.\n"
52
+ "- unsubscribe: stop/opt-out messages.\n"
53
+ "- other: anything that doesn't fit.\n\n"
54
+ "Rules:\n"
55
+ "1) Output valid JSON ONLY (no backticks, no extra text).\n"
56
+ "2) confidence in [0,1].\n"
57
+ "3) Prefer 'headlines_request' for any phrasing that implies they want today's headlines/digest.\n"
58
+ )
59
+
60
+ USER_INSTRUCTION_TEMPLATE = (
61
+ "Classify this WhatsApp message into one label.\n"
62
+ "Message: \"{text}\"\n"
63
+ "Return JSON only."
64
+ )
65
+
66
+ class ZeroShotClassifier:
67
+ def __init__(self):
68
+ self.llm = MistralTogetherClient()
69
+
70
+ def classify(self, text: str) -> Tuple[Intent, Dict]:
71
+ # Build messages
72
+ user_prompt = USER_INSTRUCTION_TEMPLATE.format(text=text.strip())
73
+ msgs = build_messages(user_prompt, SYSTEM_PROMPT)
74
+
75
+ # Call LLM (deterministic)
76
+ raw, _usage = self.llm.chat(msgs, temperature=0.0, max_tokens=200)
77
+
78
+ # Parse JSON robustly
79
+ payload = self._safe_parse_json(raw)
80
+ label = payload.get("label", "other")
81
+ confidence = float(payload.get("confidence", 0.5))
82
+ reason = payload.get("reason", "")
83
+
84
+ # Guardrails: enforce canonical label
85
+ if label not in INTENT_SET:
86
+ label = "other"
87
+
88
+ # Light normalization for obvious cases
89
+ norm = text.lower().strip()
90
+ if label == "other":
91
+ if any(s in norm for s in ["headline", "digest", "news", "today"]):
92
+ label = "headlines_request"
93
+ elif any(s in norm for s in ["unsubscribe", "opt out", "stop"]):
94
+ label = "unsubscribe"
95
+ elif re.fullmatch(r"(hi|hello|hey)[!.]?", norm):
96
+ label = "greeting"
97
+
98
+ result = {"label": label, "confidence": confidence, "reason": reason, "raw": raw}
99
+ return label, result
100
+
101
+ @staticmethod
102
+ def _safe_parse_json(s: str) -> Dict:
103
+ # Try direct parse
104
+ try:
105
+ return json.loads(s)
106
+ except Exception:
107
+ pass
108
+ # Try to extract the first {...} block
109
+ try:
110
+ start = s.find("{")
111
+ end = s.rfind("}")
112
+ if start != -1 and end != -1:
113
+ return json.loads(s[start : end + 1])
114
+ except Exception:
115
+ pass
116
+ return {}
components/LLMs/Mistral.py CHANGED
@@ -1,40 +1,79 @@
 
 
1
  import os
 
2
  import requests
3
- from typing import Optional
4
-
5
- # 🔐 Load HF credentials and endpoint URL from environment variables
6
- HF_TOKEN = os.environ.get("HF_TOKEN")
7
- MISTRAL_URL = os.environ.get("MISTRAL_URL")
8
 
9
- # 📜 Headers for HF Inference Endpoint
10
- HEADERS = {
11
- "Authorization": f"Bearer {HF_TOKEN}",
12
- "Content-Type": "application/json"
13
- }
14
 
15
- # 🔁 Call Mistral using HF Inference Endpoint
16
- def call_mistral(base_prompt: str, tail_prompt: str) -> Optional[str]:
17
- full_prompt = f"<s>[INST]{base_prompt}\n\n{tail_prompt}[/INST]</s>"
18
- payload = {
19
- "inputs": full_prompt
20
- }
21
 
22
- try:
23
- timeout = (10, 120)
24
- response = requests.post(MISTRAL_URL, headers=HEADERS, json=payload, timeout=timeout)
25
- response.raise_for_status()
26
- data = response.json()
 
 
 
 
 
 
 
 
 
27
 
28
- raw_output = ""
29
- if isinstance(data, list) and data:
30
- raw_output = data[0].get("generated_text", "")
31
- elif isinstance(data, dict):
32
- raw_output = data.get("generated_text", "")
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- if "[/INST]</s>" in raw_output:
35
- return raw_output.split("[/INST]</s>")[-1].strip()
36
- return raw_output.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- except Exception as e:
39
- print(f"⚠️ Mistral error: {e}")
40
- return None
 
 
 
 
 
 
 
1
+ # components/LLMs/Mistral.py
2
+
3
  import os
4
+ import time
5
  import requests
6
+ from typing import List, Dict, Tuple, Optional
 
 
 
 
7
 
8
+ # Env vars (set in your HF Space / .env)
9
+ TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
10
+ TOGETHER_BASE_URL = os.getenv("TOGETHER_BASE_URL", "https://api.together.xyz/v1")
11
+ TOGETHER_MODEL = os.getenv("TOGETHER_MODEL", "mistralai/Mistral-7B-Instruct-v0.3")
 
12
 
13
+ class MistralTogetherClient:
14
+ """
15
+ Wrapper around Together AI's Chat Completions API for Mistral-7B.
16
+ """
 
 
17
 
18
+ def __init__(
19
+ self,
20
+ model: str = TOGETHER_MODEL,
21
+ timeout_s: int = 25,
22
+ ):
23
+ if not TOGETHER_API_KEY:
24
+ raise RuntimeError("Missing TOGETHER_API_KEY env var")
25
+ self.model = model
26
+ self.timeout_s = timeout_s
27
+ self.url = f"{TOGETHER_BASE_URL}/chat/completions"
28
+ self.headers = {
29
+ "Authorization": f"Bearer {TOGETHER_API_KEY}",
30
+ "Content-Type": "application/json",
31
+ }
32
 
33
+ def chat(
34
+ self,
35
+ messages: List[Dict[str, str]],
36
+ temperature: float = 0.3,
37
+ max_tokens: int = 512,
38
+ ) -> Tuple[str, Dict]:
39
+ """
40
+ Send chat messages to Together's API and return (text, usage).
41
+ - messages: [{"role": "system"|"user"|"assistant", "content": "..."}]
42
+ """
43
+ payload = {
44
+ "model": self.model,
45
+ "messages": messages,
46
+ "temperature": float(temperature),
47
+ "max_tokens": int(max_tokens),
48
+ "stream": False,
49
+ }
50
 
51
+ for attempt in (1, 2): # one retry on transient errors
52
+ try:
53
+ r = requests.post(
54
+ self.url,
55
+ headers=self.headers,
56
+ json=payload,
57
+ timeout=self.timeout_s,
58
+ )
59
+ if r.status_code >= 500 or r.status_code in (408, 429):
60
+ raise RuntimeError(f"Upstream {r.status_code}: {r.text[:200]}")
61
+ r.raise_for_status()
62
+ data = r.json()
63
+ text = data["choices"][0]["message"]["content"]
64
+ usage = data.get("usage", {})
65
+ return text, usage
66
+ except Exception as e:
67
+ if attempt == 2:
68
+ raise
69
+ time.sleep(0.8) # backoff before retry
70
 
71
+ def build_messages(user_msg: str, system: Optional[str] = None) -> List[Dict[str, str]]:
72
+ """
73
+ Helper to build message arrays for chat().
74
+ """
75
+ msgs: List[Dict[str, str]] = []
76
+ if system:
77
+ msgs.append({"role": "system", "content": system})
78
+ msgs.append({"role": "user", "content": user_msg})
79
+ return msgs
components/handlers/__init__.py ADDED
File without changes
components/handlers/whatsapp_handlers.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # handlers/whatsapp_handlers.py
2
+ import logging
3
+ from fastapi.responses import JSONResponse
4
+ from components.gateways.headlines_to_wa import fetch_cached_headlines, send_to_whatsapp
5
+
6
+ def _safe_send(text: str, to: str) -> dict:
7
+ """Wrap send_to_whatsapp with logging & safe error handling."""
8
+ try:
9
+ res = send_to_whatsapp(text, destination_number=to)
10
+ if res.get("status") == "success":
11
+ logging.info(f"Sent message to {to}")
12
+ else:
13
+ logging.error(f"Failed to send message to {to}: {res}")
14
+ return res
15
+ except Exception as e:
16
+ logging.exception(f"Exception while sending WhatsApp message to {to}: {e}")
17
+ return {"status": "error", "error": str(e)}
18
+
19
+
20
+ def handle_headlines(from_number: str) -> JSONResponse:
21
+ full_message_text = fetch_cached_headlines()
22
+
23
+ if full_message_text.startswith("❌") or full_message_text.startswith("⚠️"):
24
+ logging.error(f"Failed to fetch digest for {from_number}: {full_message_text}")
25
+ _safe_send(f"Sorry, I couldn't fetch the news digest today. {full_message_text}", to=from_number)
26
+ return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to fetch digest"})
27
+
28
+ result = _safe_send(full_message_text, to=from_number)
29
+ if result.get("status") == "success":
30
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Digest sent"})
31
+ else:
32
+ _safe_send(
33
+ f"Sorry, I couldn't send the news digest to you. Error: {result.get('error', 'unknown')}",
34
+ to=from_number,
35
+ )
36
+ return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to send digest"})
37
+
38
+
39
+ def handle_preferences(from_number: str) -> JSONResponse:
40
+ msg = (
41
+ "Let’s tune your feed. Reply with topics you like:\n"
42
+ "• world • india • finance • sports • entertainment\n\n"
43
+ "You can send multiple, e.g.: india, finance"
44
+ )
45
+ _safe_send(msg, to=from_number)
46
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Preferences prompt sent"})
47
+
48
+
49
+ def handle_greeting(from_number: str) -> JSONResponse:
50
+ msg = (
51
+ "Hey! 👋 I’m NuseAI.\n"
52
+ "• Type *headlines* to get today’s digest.\n"
53
+ "• Type *preferences* to set your topics.\n"
54
+ "• Type *help* to see what I can do."
55
+ )
56
+ _safe_send(msg, to=from_number)
57
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Greeting sent"})
58
+
59
+
60
+ def handle_help(from_number: str) -> JSONResponse:
61
+ msg = (
62
+ "Here’s how I can help:\n"
63
+ "• *Headlines* — today’s 🗞️ Daily Digest 🟡\n"
64
+ "• *Preferences* — choose topics/regions\n"
65
+ "• *Unsubscribe* — stop messages\n"
66
+ "Ask me a question anytime (e.g., “What’s India’s CPI outlook?”)."
67
+ )
68
+ _safe_send(msg, to=from_number)
69
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Help sent"})
70
+
71
+
72
+ def handle_unsubscribe(from_number: str) -> JSONResponse:
73
+ _safe_send("You’re unsubscribed. If you change your mind, just say *hi*.", to=from_number)
74
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Unsubscribed"})
75
+
76
+
77
+ def handle_small_talk(from_number: str) -> JSONResponse:
78
+ _safe_send("🙂 Got it. If you’d like the news, just say *headlines*.", to=from_number)
79
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Small talk"})
80
+
81
+
82
+ def handle_chat_question(from_number: str, message_text: str) -> JSONResponse:
83
+ # Placeholder: integrate with /ask later
84
+ _safe_send(
85
+ "Great question! I’m setting up deep Q&A. For now, type *headlines* for today’s digest "
86
+ "or *preferences* to tune your feed.",
87
+ to=from_number,
88
+ )
89
+ logging.info(f"Chat question from {from_number}: {message_text}")
90
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Question acknowledged"})
routes/api/whatsapp_webhook.py CHANGED
@@ -3,86 +3,35 @@ from fastapi import APIRouter, Request, HTTPException, status
3
  from fastapi.responses import JSONResponse
4
  import logging
5
  import json
6
-
7
- # Import your function to send messages back
8
- from components.gateways.headlines_to_wa import fetch_cached_headlines, send_to_whatsapp
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
11
-
12
  router = APIRouter()
 
13
 
14
- def _extract_from_number_and_text(payload: dict):
15
- """
16
- Extracts (from_number, message_text) from a WhatsApp webhook payload.
17
- Supports text, button, and interactive replies. Silently ignores non-message webhooks.
18
- Returns (None, None) if not a user message.
19
- """
20
- try:
21
- entries = payload.get("entry", [])
22
- for entry in entries:
23
- changes = entry.get("changes", [])
24
- for change in changes:
25
- # Only process message-type changes
26
- if change.get("field") != "messages":
27
- continue
28
-
29
- value = change.get("value", {})
30
- messages_list = value.get("messages", [])
31
- if not messages_list:
32
- # This may be a status/billing event without 'messages'
33
- continue
34
-
35
- # We only look at the first message in the list for this webhook
36
- msg = messages_list[0]
37
- from_number = msg.get("from")
38
- mtype = msg.get("type")
39
-
40
- # 1) Plain text
41
- if mtype == "text":
42
- text_body = (msg.get("text", {}) or {}).get("body")
43
- if from_number and text_body:
44
- return from_number, text_body
45
-
46
- # 2) Template reply button (older/simple schema)
47
- # payload is the stable key if you set it; fallback to text/title
48
- if mtype == "button":
49
- b = msg.get("button", {}) or {}
50
- intent = b.get("payload") or b.get("text")
51
- if from_number and intent:
52
- return from_number, intent
53
-
54
- # 3) Newer interactive replies (buttons or list)
55
- if mtype == "interactive":
56
- i = msg.get("interactive", {}) or {}
57
- # Prefer stable IDs over display titles
58
- if "button_reply" in i:
59
- intent = i["button_reply"].get("id") or i["button_reply"].get("title")
60
- if from_number and intent:
61
- return from_number, intent
62
- if "list_reply" in i:
63
- intent = i["list_reply"].get("id") or i["list_reply"].get("title")
64
- if from_number and intent:
65
- return from_number, intent
66
 
67
- # If we got here, no usable user message was found
68
- return None, None
69
-
70
- except Exception:
71
- # In case of any unexpected structure, just treat as no user message
72
- return None, None
73
-
74
-
75
- # WhatsApp/Gupshup webhook endpoint
76
  @router.post("/message-received")
77
  async def whatsapp_webhook_receiver(request: Request):
78
- """
79
- Receives incoming messages from Gupshup WhatsApp webhook.
80
- Sends a daily news digest if the user sends a specific command.
81
- """
82
  try:
83
- # <<< FIX 1: Assume JSON body directly for incoming_message >>>
84
- body_bytes = await request.body()
85
- body_str = body_bytes.decode("utf-8")
86
  logging.info(f"Raw webhook body received: {body_str}")
87
 
88
  try:
@@ -91,64 +40,55 @@ async def whatsapp_webhook_receiver(request: Request):
91
  logging.error("❌ Failed to decode webhook body as JSON")
92
  return JSONResponse(status_code=400, content={"error": "Invalid JSON format"})
93
 
94
- # <<< FIX 2: Robustly extract data from the nested structure (text/button/interactive) >>>
95
  from_number, message_text = _extract_from_number_and_text(incoming_message)
96
-
97
- # Check if sender and message text were successfully extracted
98
  if not from_number or not message_text:
99
- # This is likely a status/billing webhook or a non-user message; ignore quietly
100
  logging.info("Ignoring non-message webhook or missing sender/text.")
101
  return JSONResponse(status_code=200, content={"status": "ignored", "message": "No user message"})
102
 
103
  logging.info(f"Message from {from_number}: {message_text}")
104
 
105
- # Normalize intent text lightly (don't change your core logic)
106
  normalized = message_text.lower().strip().replace("’", "'")
107
-
108
- # Check for specific commands to send the digest
109
  if normalized == "view today's headlines":
110
- logging.info(f"User {from_number} requested daily digest.")
111
-
112
- # Fetch the digest headlines
113
- full_message_text = fetch_cached_headlines()
114
-
115
- if full_message_text.startswith("❌") or full_message_text.startswith("⚠️"):
116
- logging.error(f"Failed to fetch digest for {from_number}: {full_message_text}")
117
- # Send an error message back to the user
118
- send_to_whatsapp(f"Sorry, I couldn't fetch the news digest today. {full_message_text}", destination_number=from_number)
119
- return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to fetch digest"})
120
-
121
- # Send the digest back to the user who requested it
122
- # <<< FIX 3: Pass from_number as destination_number to send_to_whatsapp >>>
123
- result = send_to_whatsapp(full_message_text, destination_number=from_number)
124
 
125
- if result.get("status") == "success":
126
- logging.info(f"✅ Successfully sent digest to {from_number}.")
127
- return JSONResponse(status_code=200, content={"status": "success", "message": "Digest sent"})
128
- else:
129
- logging.error(f" Failed to send digest to {from_number}: {result.get('error')}")
130
- send_to_whatsapp(f"Sorry, I couldn't send the news digest to you. Error: {result.get('error', 'unknown')}", destination_number=from_number)
131
- return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to send digest"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  else:
133
- logging.info(f"Received unhandled message from {from_number}: '{message_text}'")
134
- return JSONResponse(status_code=200, content={"status": "ignored", "message": "No action taken for this command"})
135
 
136
  except Exception as e:
137
  logging.error(f"Error processing webhook: {e}", exc_info=True)
138
  return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
139
 
140
- # Gupshup webhook verification endpoint (GET request with 'hub.mode' and 'hub.challenge')
141
  @router.get("/message-received")
142
  async def whatsapp_webhook_verify(request: Request):
143
- """
144
- Endpoint for Gupshup webhook verification.
145
- """
146
  mode = request.query_params.get("hub.mode")
147
  challenge = request.query_params.get("hub.challenge")
148
 
149
  if mode == "subscribe" and challenge:
150
  logging.info(f"Webhook verification successful. Challenge: {challenge}")
151
- # Challenge needs to be returned as an integer, not string
152
  return JSONResponse(status_code=200, content=int(challenge))
153
  else:
154
  logging.warning(f"Webhook verification failed. Mode: {mode}, Challenge: {challenge}")
 
3
  from fastapi.responses import JSONResponse
4
  import logging
5
  import json
6
+ from typing import Tuple, Optional
7
+
8
+ # Classifier
9
+ from components.LLMs.Classifier import ZeroShotClassifier
10
+
11
+ # Handlers
12
+ from components.handlers.whatsapp_handlers import (
13
+ handle_headlines,
14
+ handle_preferences,
15
+ handle_greeting,
16
+ handle_help,
17
+ handle_unsubscribe,
18
+ handle_small_talk,
19
+ handle_chat_question,
20
+ )
21
 
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
23
  router = APIRouter()
24
+ classifier = ZeroShotClassifier()
25
 
26
+ def _extract_from_number_and_text(payload: dict) -> Tuple[Optional[str], Optional[str]]:
27
+ ...
28
+ # (same as before unchanged)
29
+ ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
 
 
 
 
 
 
 
 
31
  @router.post("/message-received")
32
  async def whatsapp_webhook_receiver(request: Request):
 
 
 
 
33
  try:
34
+ body_str = (await request.body()).decode("utf-8")
 
 
35
  logging.info(f"Raw webhook body received: {body_str}")
36
 
37
  try:
 
40
  logging.error("❌ Failed to decode webhook body as JSON")
41
  return JSONResponse(status_code=400, content={"error": "Invalid JSON format"})
42
 
 
43
  from_number, message_text = _extract_from_number_and_text(incoming_message)
 
 
44
  if not from_number or not message_text:
 
45
  logging.info("Ignoring non-message webhook or missing sender/text.")
46
  return JSONResponse(status_code=200, content={"status": "ignored", "message": "No user message"})
47
 
48
  logging.info(f"Message from {from_number}: {message_text}")
49
 
 
50
  normalized = message_text.lower().strip().replace("’", "'")
 
 
51
  if normalized == "view today's headlines":
52
+ return handle_headlines(from_number)
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ try:
55
+ label, meta = classifier.classify(message_text)
56
+ logging.info(f"Intent classified: {label} | meta={meta}")
57
+ except Exception as e:
58
+ logging.exception(f"Classifier error: {e}")
59
+ if any(k in normalized for k in ["headline", "digest", "news", "today"]):
60
+ return handle_headlines(from_number)
61
+ return handle_help(from_number)
62
+
63
+ if label == "headlines_request":
64
+ return handle_headlines(from_number)
65
+ elif label == "preferences_update":
66
+ return handle_preferences(from_number)
67
+ elif label == "greeting":
68
+ return handle_greeting(from_number)
69
+ elif label == "help":
70
+ return handle_help(from_number)
71
+ elif label == "unsubscribe":
72
+ return handle_unsubscribe(from_number)
73
+ elif label == "small_talk":
74
+ return handle_small_talk(from_number)
75
+ elif label == "chat_question":
76
+ return handle_chat_question(from_number, message_text)
77
  else:
78
+ return handle_help(from_number)
 
79
 
80
  except Exception as e:
81
  logging.error(f"Error processing webhook: {e}", exc_info=True)
82
  return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
83
 
84
+
85
  @router.get("/message-received")
86
  async def whatsapp_webhook_verify(request: Request):
 
 
 
87
  mode = request.query_params.get("hub.mode")
88
  challenge = request.query_params.get("hub.challenge")
89
 
90
  if mode == "subscribe" and challenge:
91
  logging.info(f"Webhook verification successful. Challenge: {challenge}")
 
92
  return JSONResponse(status_code=200, content=int(challenge))
93
  else:
94
  logging.warning(f"Webhook verification failed. Mode: {mode}, Challenge: {challenge}")