ragV98 commited on
Commit
1d4183c
·
1 Parent(s): 5e03773

eli5 response quality revamp

Browse files
components/handlers/whatsapp_handlers.py CHANGED
@@ -1,12 +1,12 @@
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
  # ------------------------------------------------------------
@@ -98,7 +98,7 @@ def handle_small_talk(from_number: str) -> JSONResponse:
98
 
99
 
100
  # ------------------------------------------------------------
101
- # Chat Question → “Explain by number” flow
102
  # ------------------------------------------------------------
103
 
104
  _HEADLINE_LINE_RE = re.compile(r"^\s*(\d+)\.\s+(.*)$")
@@ -142,18 +142,28 @@ def _parse_rendered_digest(rendered: str) -> Dict[int, str]:
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 = (
@@ -164,33 +174,99 @@ def _retrieve_context_for_headline(headline_text: str, top_k: int = 5) -> str:
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:
@@ -199,8 +275,8 @@ def handle_chat_question(from_number: str, message_text: str) -> JSONResponse:
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}")
@@ -221,12 +297,12 @@ def handle_chat_question(from_number: str, message_text: str) -> JSONResponse:
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)
 
1
  # handlers/whatsapp_handlers.py
2
  import logging
3
+ import os
4
  import re
5
  from typing import Optional, Dict
6
 
7
  from fastapi.responses import JSONResponse
8
 
9
  from components.gateways.headlines_to_wa import fetch_cached_headlines, send_to_whatsapp
 
10
  from components.LLMs.Mistral import MistralTogetherClient, build_messages
11
 
12
  # ------------------------------------------------------------
 
98
 
99
 
100
  # ------------------------------------------------------------
101
+ # Chat Question → “Explain by number” flow (structured + quality-guarded)
102
  # ------------------------------------------------------------
103
 
104
  _HEADLINE_LINE_RE = re.compile(r"^\s*(\d+)\.\s+(.*)$")
 
142
  return mapping
143
 
144
 
145
+ def _retrieve_context_for_headline(headline_text: str, top_k: int = 15) -> str:
146
  """
147
  Use the vector index to pull contextual passages related to the headline.
148
+ - Uses a higher top_k to widen coverage (quality over speed).
149
+ - Gracefully degrades if index is unavailable or not yet built.
150
  """
151
+ # Defer the import so a missing/invalid index module won't break imports
152
+ try:
153
+ from components.indexers.news_indexer import load_news_index # type: ignore
154
+ except Exception as e:
155
+ logging.warning(f"Index module not available yet: {e}")
156
+ return ""
157
+
158
+ # Try to load the index; if persist_dir is wrong/missing, swallow and return ""
159
  try:
160
  index = load_news_index()
161
  try:
162
+ # LlamaIndex v0.10+
163
  qe = index.as_query_engine(similarity_top_k=top_k)
164
  except Exception:
165
+ # Older API fallback
166
+ from llama_index.core.query_engine import RetrievalQueryEngine # type: ignore
167
  qe = RetrievalQueryEngine(index=index, similarity_top_k=top_k)
168
 
169
  query = (
 
174
  resp = qe.query(query)
175
  return str(resp)
176
  except Exception as e:
177
+ # Avoid noisy tracebacks in normal operation; index may simply not exist yet
178
+ persist_dir = os.getenv("NEWS_INDEX_PERSIST_DIR") or os.getenv("PERSIST_DIR") or "<unset>"
179
+ logging.warning(f"Vector retrieval skipped (no index at {persist_dir}): {e}")
180
  return ""
181
 
182
 
183
+ def _eli5_answer_structured(question: str, context: str, headline_only: Optional[str] = None) -> str:
184
  """
185
+ Generate a structured, quality-guarded ELI5 answer.
186
+ Format:
187
+ Headline #N — <short title>
188
+ Key points:
189
+ • ...
190
+ • ...
191
+ Numbers & facts:
192
+ • ...
193
+ Why it matters:
194
+ • ...
195
+ Caveats:
196
+ • ...
197
+ Confidence: High/Medium/Low
198
+
199
+ Rules:
200
+ - 120–180 words total.
201
+ - Use ONLY the provided context/headline; if missing, write “Not in context”.
202
+ - No speculation; keep neutral tone; be brief.
203
  """
204
  sys_prompt = (
205
+ "You are a rigorous, concise explainer for a news assistant. "
206
+ "Produce clear, structured outputs with bullet points. "
207
+ "If any detail is not present in context, write 'Not in context'. "
208
+ "Avoid flowery language; be factual and neutral."
 
 
 
 
209
  )
210
 
211
+ if context.strip():
212
+ user_prompt = (
213
+ f"QUESTION:\n{question}\n\n"
214
+ f"CONTEXT (may be partial, use ONLY this):\n{context}\n\n"
215
+ "Write 120–180 words in this exact structure:\n"
216
+ "Headline:\n"
217
+ "Key points:\n"
218
+ "• ...\n• ...\n• ...\n"
219
+ "Numbers & facts:\n"
220
+ "• ...\n• ...\n"
221
+ "Why it matters:\n"
222
+ "• ...\n"
223
+ "Caveats:\n"
224
+ "• ...\n"
225
+ "Confidence: High | Medium | Low\n"
226
+ "Rules:\n"
227
+ "- If you can't find a detail in CONTEXT, write 'Not in context'.\n"
228
+ "- Do NOT add sources or links unless they appear in CONTEXT.\n"
229
+ "- Keep it short, precise, and neutral.\n"
230
+ )
231
+ else:
232
+ # fallback: rely on the headline only
233
+ headline_text = headline_only or question
234
+ user_prompt = (
235
+ "CONTEXT is empty. You must base the answer ONLY on the HEADLINE below; "
236
+ "write 'Not in context' for any missing specifics.\n\n"
237
+ f"HEADLINE:\n{headline_text}\n\n"
238
+ "Write 90–140 words in this exact structure:\n"
239
+ "Headline:\n"
240
+ "Key points:\n"
241
+ "• ...\n• ...\n"
242
+ "Numbers & facts:\n"
243
+ "• Not in context\n"
244
+ "Why it matters:\n"
245
+ "• ...\n"
246
+ "Caveats:\n"
247
+ "• Limited details available\n"
248
+ "Confidence: Low\n"
249
+ )
250
+
251
  try:
252
  llm = MistralTogetherClient()
253
  msgs = build_messages(user_prompt, sys_prompt)
254
+ out, _usage = llm.chat(msgs, temperature=0.2, max_tokens=400)
255
  return out.strip()
256
  except Exception as e:
257
+ logging.exception(f"Mistral structured ELI5 generation failed: {e}")
258
+ return (
259
+ "Headline:\n"
260
+ "Key points:\n"
261
+ "• I couldn’t generate an explanation right now.\n"
262
+ "Numbers & facts:\n"
263
+ "• Not in context\n"
264
+ "Why it matters:\n"
265
+ "• Not in context\n"
266
+ "Caveats:\n"
267
+ "• System error\n"
268
+ "Confidence: Low"
269
+ )
270
 
271
 
272
  def handle_chat_question(from_number: str, message_text: str) -> JSONResponse:
 
275
  - If the user references a headline number (“explain 14 like I’m 5”),
276
  1) Parse the number
277
  2) Look up that numbered line from the rendered digest
278
+ 3) Retrieve vector context (top_k widened for coverage)
279
+ 4) Generate a STRUCTURED ELI5 answer (with quality guardrails)
280
  - Otherwise, provide a gentle hint (for now).
281
  """
282
  logging.info(f"Chat question from {from_number}: {message_text}")
 
297
  )
298
  return JSONResponse(status_code=200, content={"status": "success", "message": "Number not found"})
299
 
300
+ # 3) Retrieve broader context from the vector index using the headline line
301
+ ctx = _retrieve_context_for_headline(target_line, top_k=15)
302
 
303
+ # 4) Generate STRUCTURED ELI5 answer (works even if ctx == "")
304
  question = f"Explain headline #{number}: {target_line}"
305
+ answer = _eli5_answer_structured(question, ctx, headline_only=target_line)
306
 
307
  # 5) Send back
308
  _safe_send(answer, to=from_number)