ragV98 commited on
Commit
5908e3c
·
1 Parent(s): 5776f9f

handling headline realted questions

Browse files
components/handlers/whatsapp_handlers.py CHANGED
@@ -1,7 +1,17 @@
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."""
@@ -17,6 +27,10 @@ def _safe_send(text: str, to: str) -> dict:
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
 
@@ -36,6 +50,10 @@ def handle_headlines(from_number: str) -> JSONResponse:
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"
@@ -79,12 +97,145 @@ def handle_small_talk(from_number: str) -> JSONResponse:
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 todays 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"})
 
1
  # handlers/whatsapp_handlers.py
2
  import logging
3
+ import re
4
+ from typing import Optional, Dict
5
+
6
  from fastapi.responses import JSONResponse
7
+
8
  from components.gateways.headlines_to_wa import fetch_cached_headlines, send_to_whatsapp
9
+ from components.indexers.news_indexer import load_news_index # should return a LlamaIndex VectorStoreIndex
10
+ from components.LLMs.Mistral import MistralTogetherClient, build_messages
11
+
12
+ # ------------------------------------------------------------
13
+ # Utilities
14
+ # ------------------------------------------------------------
15
 
16
  def _safe_send(text: str, to: str) -> dict:
17
  """Wrap send_to_whatsapp with logging & safe error handling."""
 
27
  return {"status": "error", "error": str(e)}
28
 
29
 
30
+ # ------------------------------------------------------------
31
+ # Headlines
32
+ # ------------------------------------------------------------
33
+
34
  def handle_headlines(from_number: str) -> JSONResponse:
35
  full_message_text = fetch_cached_headlines()
36
 
 
50
  return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to send digest"})
51
 
52
 
53
+ # ------------------------------------------------------------
54
+ # Preferences / Greeting / Help / Unsubscribe / Small Talk
55
+ # ------------------------------------------------------------
56
+
57
  def handle_preferences(from_number: str) -> JSONResponse:
58
  msg = (
59
  "Let’s tune your feed. Reply with topics you like:\n"
 
97
  return JSONResponse(status_code=200, content={"status": "success", "message": "Small talk"})
98
 
99
 
100
+ # ------------------------------------------------------------
101
+ # Chat Question → “Explain by number” flow
102
+ # ------------------------------------------------------------
103
+
104
+ _HEADLINE_LINE_RE = re.compile(r"^\s*(\d+)\.\s+(.*)$")
105
+
106
+ def _extract_number_ref(text: str) -> Optional[int]:
107
+ """
108
+ Find a referenced headline number in free text, e.g.:
109
+ 'explain number 14', 'no. 7 please', '#9', '14', 'explain 14 like I am 5'
110
+ Returns int or None.
111
+ """
112
+ s = (text or "").lower()
113
+
114
+ # explicit forms
115
+ m = re.search(r"(?:number|no\.?|num|#)\s*(\d+)", s)
116
+ if m:
117
+ return int(m.group(1))
118
+
119
+ # a bare number (avoid picking up years like 2025; cap at 1..200)
120
+ m2 = re.search(r"\b(\d{1,3})\b", s)
121
+ if m2:
122
+ n = int(m2.group(1))
123
+ if 1 <= n <= 200:
124
+ return n
125
+
126
+ return None
127
+
128
+
129
+ def _parse_rendered_digest(rendered: str) -> Dict[int, str]:
130
+ """
131
+ Parse the same rendered digest string you send on WhatsApp and build a map:
132
+ { number -> headline_line_text }
133
+ """
134
+ mapping: Dict[int, str] = {}
135
+ for line in (rendered or "").splitlines():
136
+ m = _HEADLINE_LINE_RE.match(line)
137
+ if not m:
138
+ continue
139
+ num = int(m.group(1))
140
+ headline_txt = m.group(2).strip()
141
+ mapping[num] = headline_txt
142
+ return mapping
143
+
144
+
145
+ def _retrieve_context_for_headline(headline_text: str, top_k: int = 5) -> str:
146
+ """
147
+ Use the vector index to pull contextual passages related to the headline.
148
+ Gracefully degrades if index is unavailable.
149
+ """
150
+ try:
151
+ index = load_news_index()
152
+ try:
153
+ qe = index.as_query_engine(similarity_top_k=top_k)
154
+ except Exception:
155
+ # Older LlamaIndex fallback
156
+ from llama_index.core.query_engine import RetrievalQueryEngine
157
+ qe = RetrievalQueryEngine(index=index, similarity_top_k=top_k)
158
+
159
+ query = (
160
+ "Retrieve concise, factual context that best explains this headline:\n"
161
+ f"{headline_text}\n"
162
+ "Focus on who/what/when/where/why, include crucial numbers, avoid speculation."
163
+ )
164
+ resp = qe.query(query)
165
+ return str(resp)
166
+ except Exception as e:
167
+ logging.exception(f"Vector retrieval failed: {e}")
168
+ return ""
169
+
170
+
171
+ def _eli5_answer(question: str, context: str) -> str:
172
+ """
173
+ Ask Mistral (via Together) to explain using simple words.
174
+ """
175
+ sys_prompt = (
176
+ "You are a concise explainer for a news assistant. "
177
+ "Answer like the person is 5 years old (ELI5): short sentences, simple words, "
178
+ "and 3–6 bullet points max. Be accurate and neutral. If unsure, say so."
179
+ )
180
+ user_prompt = (
181
+ f"QUESTION:\n{question}\n\n"
182
+ f"CONTEXT (may be partial):\n{context}\n\n"
183
+ "Now give a short ELI5 explanation. Avoid jargon. If numbers matter, include them."
184
+ )
185
+
186
+ try:
187
+ llm = MistralTogetherClient()
188
+ msgs = build_messages(user_prompt, sys_prompt)
189
+ out, _usage = llm.chat(msgs, temperature=0.2, max_tokens=350)
190
+ return out.strip()
191
+ except Exception as e:
192
+ logging.exception(f"Mistral ELI5 generation failed: {e}")
193
+ return "I couldn’t generate a simple explanation right now. Please try again."
194
+
195
+
196
  def handle_chat_question(from_number: str, message_text: str) -> JSONResponse:
197
+ """
198
+ Smart handler:
199
+ - If the user references a headline number (“explain 14 like I’m 5”),
200
+ 1) Parse the number
201
+ 2) Look up that numbered line from the rendered digest
202
+ 3) Retrieve vector context
203
+ 4) Generate an ELI5 answer with Mistral (Together.ai)
204
+ - Otherwise, provide a gentle hint (for now).
205
+ """
206
+ logging.info(f"Chat question from {from_number}: {message_text}")
207
+
208
+ # 1) Try to find a headline number reference
209
+ number = _extract_number_ref(message_text or "")
210
+ if number is not None:
211
+ # 2) Load rendered digest and map numbers to lines
212
+ rendered = fetch_cached_headlines()
213
+ mapping = _parse_rendered_digest(rendered)
214
+ target_line = mapping.get(number)
215
+
216
+ if not target_line:
217
+ _safe_send(
218
+ f"I couldn’t find headline *{number}* in today’s digest. "
219
+ "Try another number or say *headlines* to see today’s list.",
220
+ to=from_number,
221
+ )
222
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Number not found"})
223
+
224
+ # 3) Retrieve context from the vector index using the headline line
225
+ ctx = _retrieve_context_for_headline(target_line, top_k=5)
226
+
227
+ # 4) Generate ELI5 answer
228
+ question = f"Explain headline #{number}: {target_line}"
229
+ answer = _eli5_answer(question, ctx)
230
+
231
+ # 5) Send back
232
+ _safe_send(answer, to=from_number)
233
+ return JSONResponse(status_code=200, content={"status": "success", "message": "ELI5 sent"})
234
+
235
+ # No number found → for now, guide the user
236
  _safe_send(
237
+ "Ask me about a specific headline by number, e.g., *explain 7 like Im 5*.\n"
238
+ "Or type *headlines* for today’s digest.",
239
  to=from_number,
240
  )
241
+ return JSONResponse(status_code=200, content={"status": "success", "message": "Generic reply"})