rairo commited on
Commit
e9a8c8d
·
verified ·
1 Parent(s): ac4b257

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +173 -142
main.py CHANGED
@@ -3,28 +3,23 @@ import os
3
  import logging
4
  import json
5
  from dotenv import load_dotenv
6
- from datetime import datetime, timedelta, timezone
7
  from time import time
8
  import threading
9
  from collections import OrderedDict
10
  import uuid
11
  import re
12
- from typing import Optional, Dict, Any, List # IMPORTED Dict, List, Any
13
 
14
- # Tunasonga specific utility functions
15
  import utility_tunasonga as tunasonga_utils
16
-
17
- # WhatsApp client for sending messages
18
  import whatsapp_client as wa_client
19
 
20
- # Firebase Admin SDK
21
  from firebase_admin import credentials, db, initialize_app, get_app, exceptions as firebase_exceptions
22
  import firebase_admin
23
 
24
- # AssemblyAI
25
  import assemblyai as aai
26
 
27
- load_dotenv()
28
  app = Flask(__name__)
29
 
30
  logging.basicConfig(
@@ -46,17 +41,20 @@ try:
46
  cred_wh = credentials.Certificate(credentials_json_wh)
47
  app_name_wh = "TunasongaWhatsAppWebhookApp"
48
 
49
- app_exists = False
50
  if firebase_admin._apps:
51
- for existing_app_name_key in firebase_admin._apps:
52
- if existing_app_name_key == app_name_wh:
53
- app_exists = True
54
- break
 
 
 
 
 
55
 
56
- if not app_exists:
57
  firebase_app_instance = initialize_app(cred_wh, {'databaseURL': FIREBASE_DB_URL_WH}, name=app_name_wh)
58
- else:
59
- firebase_app_instance = get_app(name=app_name_wh)
60
 
61
  FIREBASE_WH_INITIALIZED = True
62
  logger.info(f"Webhook: Firebase Admin SDK initialized successfully (App: {firebase_app_instance.name}).")
@@ -67,18 +65,20 @@ except Exception as e:
67
 
68
  # --- Initialize Utility Modules with necessary configs ---
69
  if FIREBASE_WH_INITIALIZED:
70
- # This function in utility_tunasonga.py should initialize GEMINI_LLM_CLIENT
71
- # as genai.GenerativeModel("gemini-2.0-flash-001")
72
- tunasonga_utils.initialize_gemini_for_utility(os.getenv('GOOGLE_API_KEY'), model_name="gemini-2.0-flash-001")
 
73
  else:
74
  logger.critical("Webhook: Firebase not initialized, utility functions requiring DB or AI may fail. AI parsing will be disabled.")
75
  tunasonga_utils.GEMINI_LLM_CLIENT = None
76
 
77
-
78
  # --- WhatsApp & Other Configurations ---
79
- WHATSAPP_VERIFY_TOKEN = "30cca545-3838-48b2-80a7-9e43b1ae8ce4"
80
- if not WHATSAPP_VERIFY_TOKEN:
81
- logger.critical("WHATSAPP_VERIFY_TOKEN is not set in environment variables! Webhook verification will fail.")
 
 
82
 
83
  AAI_API_KEY = os.getenv("AAI_KEY")
84
  transcriber_aai = None
@@ -92,7 +92,7 @@ if AAI_API_KEY:
92
  else:
93
  logger.warning("Webhook: AAI_KEY not found. Audio transcription disabled for webhook.")
94
 
95
- # --- Duplicate Message Handling ---
96
  class MessageDeduplicator:
97
  def __init__(self, ttl_hours=24, max_cache_size=10000, db_app_instance_for_dedup=None):
98
  self.ttl_seconds = ttl_hours * 3600
@@ -104,37 +104,45 @@ class MessageDeduplicator:
104
  if self.db_app_instance:
105
  self._cleanup_thread.start()
106
  else:
107
- logger.warning("MessageDeduplicator: DB app instance not provided, persistence for processed message IDs disabled.")
 
 
 
 
108
 
109
  def is_duplicate(self, message_id: str) -> bool:
110
  if not message_id: return False
 
111
  with self.lock:
112
- if message_id in self.cache:
113
- self.cache.move_to_end(message_id)
114
  return True
115
  if self.db_app_instance:
116
  try:
117
- doc_ref = db.reference(f'whatsapp_processed_messages/{message_id}', app=self.db_app_instance)
118
  doc = doc_ref.get()
119
  if doc:
120
- self.cache[message_id] = time()
121
  if len(self.cache) > self.max_cache_size: self.cache.popitem(last=False)
122
  return True
123
- except Exception as e: logger.error(f"Deduplicator DB error in is_duplicate: {e}", exc_info=True)
 
124
  self._mark_processed(message_id)
125
  return False
126
 
127
  def _mark_processed(self, message_id: str):
128
  current_time = time()
 
129
  with self.lock:
130
- self.cache[message_id] = current_time
131
  if len(self.cache) > self.max_cache_size: self.cache.popitem(last=False)
132
  if self.db_app_instance:
133
  try:
134
- doc_ref = db.reference(f'whatsapp_processed_messages/{message_id}', app=self.db_app_instance)
135
- doc_ref.set({'timestamp': current_time, 'processed_at_iso': datetime.now(timezone.utc).isoformat()})
136
- except Exception as e: logger.error(f"Deduplicator DB error in _mark_processed: {e}", exc_info=True)
137
-
 
138
  def _periodic_cleanup(self):
139
  while True:
140
  try:
@@ -147,6 +155,20 @@ class MessageDeduplicator:
147
  for msg_id in expired_ids_mem:
148
  self.cache.pop(msg_id, None)
149
  if expired_ids_mem: logger.info(f"Deduplicator: Cleaned {len(expired_ids_mem)} expired IDs from in-memory cache.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  threading.Event().wait(3600)
151
  except Exception as e:
152
  logger.error(f"Error in Deduplicator cleanup thread: {e}", exc_info=True)
@@ -164,7 +186,6 @@ def check_and_mark_processed_whatsapp_message(message_id: str) -> bool:
164
  return message_deduplicator.is_duplicate(message_id)
165
 
166
  # --- Core Webhook Logic ---
167
-
168
  def handle_user_action_confirmation(user_profile: Dict[str, Any], action_id_from_button: str, mobile_from_user: str):
169
  if not FIREBASE_WH_INITIALIZED:
170
  wa_client.send_text_message(mobile_from_user, "Sorry, our system is temporarily unable to process confirmations.")
@@ -172,123 +193,133 @@ def handle_user_action_confirmation(user_profile: Dict[str, Any], action_id_from
172
 
173
  try:
174
  parts = action_id_from_button.split('_', 2)
175
- if len(parts) < 3: raise IndexError("Button ID format incorrect")
176
- confirm_or_cancel = parts[0]
177
- action_type_from_button_raw = parts[1]
 
 
 
 
178
  pending_action_id = parts[2]
179
  except IndexError:
180
- logger.warning(f"Invalid button ID format: {action_id_from_button} from {mobile_from_user}")
181
  wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't understand that button press.")
182
  return
183
 
184
- pending_action_data = tunasonga_utils.retrieve_pending_action(mobile_from_user, pending_action_id, firebase_app_instance)
185
 
186
- if not pending_action_data:
187
  wa_client.send_text_message(mobile_from_user, "It seems this action has expired or was already processed. Please try again if needed.")
188
  return
189
 
190
- stored_action_type = pending_action_data.get("data", {}).get("action_type_original", action_type_from_button_raw)
 
 
191
 
192
  if confirm_or_cancel == "confirm":
193
- logger.info(f"User {mobile_from_user} (UID: {user_profile.get('uid')}) confirmed action: {stored_action_type} with ID {pending_action_id}")
194
- action_data_entities = pending_action_data.get("data", {})
195
  response_message = "Action confirmed and processed!"
196
 
197
- if stored_action_type == "listproduce":
198
  listing_id = str(uuid.uuid4())
199
  listing_payload = {
200
- 'lister_id': user_profile.get('uid'),
201
- 'listing_type': 'produce', 'status': 'pending_approval',
202
- 'created_at': datetime.now(timezone.utc).isoformat(), 'whatsapp_source': True,
203
- 'crop_type': action_data_entities.get('crop_type'),
204
- 'quantity': action_data_entities.get('quantity'), 'unit': action_data_entities.get('unit'),
205
- 'asking_price': action_data_entities.get('price') if action_data_entities.get('price_type') == 'per_unit' else None,
206
- 'total_asking_price': action_data_entities.get('price') if action_data_entities.get('price_type') == 'total' else None,
207
- 'currency': action_data_entities.get('currency'),
208
- 'location': action_data_entities.get('location') or user_profile.get('location', 'Unknown'),
209
- 'availability_description': action_data_entities.get('availability_description'),
210
- 'quality_description': action_data_entities.get('quality_description')
211
  }
212
  listing_payload_cleaned = {k: v for k, v in listing_payload.items() if v is not None}
213
  try:
214
  db.reference(f'listings/{listing_id}', app=firebase_app_instance).set(listing_payload_cleaned)
215
- response_message = f"Great! Your produce listing for {action_data_entities.get('quantity')} {action_data_entities.get('unit','')} of {action_data_entities.get('crop_type')} has been submitted. Admin may review it."
216
  except Exception as e_db:
217
  logger.error(f"DB error creating listing for {mobile_from_user}: {e_db}", exc_info=True)
218
  response_message = "Sorry, there was an error submitting your listing. Please try again."
219
 
220
- elif stored_action_type == "postdemand":
221
  demand_id = str(uuid.uuid4())
222
  demand_payload = {
223
  'lister_id': user_profile.get('uid'), 'listing_type': 'demand',
224
  'status': 'pending_approval', 'created_at': datetime.now(timezone.utc).isoformat(),
225
- 'whatsapp_source': True, 'crop_type': action_data_entities.get('crop_type'),
226
- 'quantity': action_data_entities.get('quantity'), 'unit': action_data_entities.get('unit'),
227
- 'price_range': f"{action_data_entities.get('currency','')}{action_data_entities.get('price')} ({action_data_entities.get('price_type')})" if action_data_entities.get('price') else "Not specified",
228
- 'location': action_data_entities.get('location') or user_profile.get('location', 'Unknown'),
229
- 'needed_by_description': action_data_entities.get('availability_description'),
230
- 'quality_specs': action_data_entities.get('quality_description')
231
  }
232
  demand_payload_cleaned = {k: v for k, v in demand_payload.items() if v is not None}
233
  try:
234
  db.reference(f'listings/{demand_id}', app=firebase_app_instance).set(demand_payload_cleaned)
235
- response_message = f"Okay! Your demand for {action_data_entities.get('quantity')} {action_data_entities.get('unit','')} of {action_data_entities.get('crop_type')} has been posted. Admin may review it."
236
  except Exception as e_db:
237
  logger.error(f"DB error creating demand for {mobile_from_user}: {e_db}", exc_info=True)
238
  response_message = "Sorry, there was an error posting your demand. Please try again."
239
  else:
240
- response_message = f"Confirmed! However, I'm not sure how to process action type '{stored_action_type}' yet."
 
241
  wa_client.send_text_message(mobile_from_user, response_message)
242
 
243
  elif confirm_or_cancel == "cancel":
244
- logger.info(f"User {mobile_from_user} cancelled action: {stored_action_type} with ID {pending_action_id}")
245
  wa_client.send_text_message(mobile_from_user, "Okay, I've cancelled that action. Let me know if there's anything else!")
246
  else:
247
  logger.warning(f"Unknown confirmation command '{confirm_or_cancel}' for action ID {pending_action_id} from {mobile_from_user}")
248
  wa_client.send_text_message(mobile_from_user, "Sorry, I didn't understand that confirmation.")
 
249
  tunasonga_utils.delete_pending_action(mobile_from_user, pending_action_id, firebase_app_instance)
250
 
251
 
252
  def process_whatsapp_text_message(user_profile: Dict[str, Any], message_text: str, mobile_from_user: str):
253
- logger.info(f"Processing text from UID {user_profile.get('uid')} ({mobile_from_user}): '{message_text}'")
 
254
 
255
- if re.match(r'^\s*(hi|hello|menu|help|options|sawubona|mhoro|salibonani)\b.*$', message_text, re.IGNORECASE):
 
256
  wa_client.send_text_message(mobile_from_user,
257
- f"Hi {user_profile.get('name', 'there')}! Welcome to Tunasonga Agri WhatsApp.\n"
258
  "You can:\n"
259
- "🌾 List produce: e.g., 'Sell 10 bags maize'\n"
260
- "🛒 Post demand: e.g., 'Need 50kg tomatoes'\n"
261
  "📋 Check your listings: 'My listings'\n"
262
  "🤝 Check your deals: 'My deals'\n"
263
- "💡 Ask questions: e.g., 'Maize price trends?' or 'How to store potatoes?'"
 
264
  )
265
  return
266
 
267
- parsed_llm_response = tunasonga_utils.parse_user_request_with_gemini(message_text) # This uses GEMINI_LLM_CLIENT from utility
268
 
269
  if not parsed_llm_response or "error" in parsed_llm_response or not tunasonga_utils.GEMINI_LLM_CLIENT:
270
  error_detail = parsed_llm_response.get("error", "Could not understand request") if parsed_llm_response else "Could not understand request"
271
  if not tunasonga_utils.GEMINI_LLM_CLIENT: error_detail = "AI parsing service unavailable."
272
- wa_client.send_text_message(mobile_from_user, f"Sorry, I had trouble understanding that. ({error_detail}). Could you try rephrasing clearly?")
 
273
  return
274
 
275
  intent = parsed_llm_response.get("intent", "unknown")
276
  entities = parsed_llm_response.get("entities", {})
277
- logger.info(f"LLM parsed intent: {intent}, entities: {entities} for {mobile_from_user}")
278
 
279
  if intent == "list_produce" or intent == "post_demand":
280
  crop = entities.get("crop_type")
281
  qty = entities.get("quantity")
 
282
  if not crop or qty is None:
283
  missing_info = []
284
- if not crop: missing_info.append("crop type")
285
- if qty is None: missing_info.append("quantity")
286
- wa_client.send_text_message(mobile_from_user, f"To {intent.replace('_', ' ')}, I need at least the {', '.join(missing_info)}. Can you provide that?")
287
  return
 
288
  pending_action_id = str(uuid.uuid4())
289
- action_type_confirm = "listproduce" if intent == "list_produce" else "postdemand"
290
- data_to_persist = {"action_type_original": action_type_confirm, **entities}
291
- if tunasonga_utils.persist_pending_action(mobile_from_user, pending_action_id, data_to_persist, firebase_app_instance):
292
  summary = f"Action: {intent.replace('_',' ').title()}\n"
293
  summary += f"- Crop: {entities.get('crop_type', 'N/A')}\n"
294
  summary += f"- Quantity: {entities.get('quantity', 'N/A')} {entities.get('unit', '')}\n"
@@ -297,35 +328,43 @@ def process_whatsapp_text_message(user_profile: Dict[str, Any], message_text: st
297
  if entities.get('location'): summary += f"- Location: {entities.get('location')}\n"
298
  if entities.get('availability_description'): summary += f"- Availability: {entities.get('availability_description')}\n"
299
  if entities.get('quality_description'): summary += f"- Quality: {entities.get('quality_description')}\n"
 
300
  buttons = [
301
- {"reply": {"id": f"confirm_{action_type_confirm}_{pending_action_id}", "title": "✅ Yes, Proceed"}},
302
- {"reply": {"id": f"cancel_{action_type_confirm}_{pending_action_id}", "title": "❌ No, Cancel"}}
303
  ]
304
- body_text = f"Okay, I understand you want to:\n\n{summary.strip()}\n\nIs this correct?"
305
  wa_client.send_reply_buttons(mobile_from_user, body_text, buttons)
306
  else:
307
- wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't save your request for confirmation. Please try again.")
308
 
309
  elif intent == "get_my_listings":
 
 
 
 
310
  listings_ref = db.reference('listings', app=firebase_app_instance)\
311
  .order_by_child('lister_id')\
312
- .equal_to(user_profile.get('uid'))\
313
  .get()
314
  active_listings = {lid: ldata for lid, ldata in (listings_ref or {}).items() if isinstance(ldata, dict) and ldata.get('status') != 'closed'}
315
- response_text = tunasonga_utils.format_listings_for_whatsapp(active_listings, "produce/demand")
316
  wa_client.send_text_message(mobile_from_user, response_text)
317
 
318
  elif intent == "get_my_deals":
 
 
 
 
319
  all_deals = db.reference('deals', app=firebase_app_instance).get() or {}
320
  my_deals = {}
321
- user_uid = user_profile.get('uid')
322
  for deal_id, deal_data in all_deals.items():
323
  if isinstance(deal_data, dict) and \
324
- (deal_data.get('buyer_id') == user_uid or \
325
- deal_data.get('farmer_id') == user_uid or \
326
- deal_data.get('assigned_transporter_id') == user_uid):
327
  my_deals[deal_id] = deal_data
328
- response_text = tunasonga_utils.format_deals_for_whatsapp(my_deals, user_uid)
329
  wa_client.send_text_message(mobile_from_user, response_text)
330
 
331
  elif intent == "get_platform_demands" or intent == "get_platform_produce":
@@ -333,63 +372,52 @@ def process_whatsapp_text_message(user_profile: Dict[str, Any], message_text: st
333
  query_ref = db.reference('listings', app=firebase_app_instance)\
334
  .order_by_child('listing_type')\
335
  .equal_to(listing_type_filter)
336
- all_ofType = query_ref.get() or {}
 
337
  filtered_listings = {}
338
  target_crop = entities.get("crop_type", "").lower() if entities.get("crop_type") else None
 
339
  for lid, ldata in all_ofType.items():
340
  if isinstance(ldata, dict) and ldata.get('status') == 'active':
341
  if target_crop:
342
  if target_crop in ldata.get("crop_type", "").lower():
343
  filtered_listings[lid] = ldata
344
- else: filtered_listings[lid] = ldata
 
 
345
  if not filtered_listings:
346
- response_text = f"No active {listing_type_filter} listings found"
347
  if target_crop: response_text += f" for {entities.get('crop_type')}"
348
- response_text += "."
349
  else:
350
- response_text = f"Found these active {listing_type_filter} listings"
351
  if target_crop: response_text += f" for {entities.get('crop_type')}"
352
- response_text += ":\n"
353
- response_text += tunasonga_utils.format_listings_for_whatsapp(dict(list(filtered_listings.items())[:5]), listing_type_filter)
354
- if len(filtered_listings) > 5: response_text += "\n...and more. Ask for specific locations or refine your search."
355
  wa_client.send_text_message(mobile_from_user, response_text)
356
 
357
  elif intent == "ai_chat_query":
358
  original_query = entities.get("original_query_for_ai_chat", message_text)
359
- logger.info(f"Webhook: Handling direct AI chat query for {mobile_from_user} (UID: {user_profile.get('uid')}): '{original_query}'")
360
 
361
- if tunasonga_utils.GEMINI_LLM_CLIENT: # This is now the initialized GenerativeModel instance
362
  try:
363
- # Construct a prompt suitable for general agricultural advice for the SADC region
364
- # The GEMINI_LLM_CLIENT is already initialized with a model name (e.g., "gemini-1.5-flash-latest")
365
- # in utility_tunasonga.py
366
-
367
- # The prompt for general AI chat should be distinct from the intent parsing prompt.
368
- ai_chat_system_prompt = "You are Tunasonga Agri Assistant, an expert in agriculture in the SADC region, focusing on smallholder farmers. Provide a helpful, concise, and practical answer to the following user question. If the question is outside of agriculture, farming, or agri-business in the SADC context, politely state that you can primarily assist with agricultural topics."
369
-
370
- # Combine system instruction with user query for the model
371
- # For GenerativeModel, contents can be a list of strings or Parts.
372
- # A simple way is to concatenate.
373
- full_chat_prompt = f"{ai_chat_system_prompt}\n\nUser question: \"{original_query}\"\n\nAnswer:"
374
 
375
  gemini_response = tunasonga_utils.GEMINI_LLM_CLIENT.generate_content(full_chat_prompt)
376
 
377
  response_text = gemini_response.text.strip() if gemini_response.text else "I received an empty response from the AI assistant. Could you try asking in a different way?"
378
 
379
- # Store this AI interaction in the user's main AI chat history
380
- try:
381
  db.reference(f'ai_chat_history/{user_profile.get("uid")}/{str(uuid.uuid4())}', app=firebase_app_instance).set({
382
- 'user_message': original_query,
383
- 'ai_response': response_text,
384
  'intent_classified': 'whatsapp_direct_ai_query',
385
- 'timestamp': datetime.now(timezone.utc).isoformat(),
386
- 'source': 'whatsapp'
387
  })
388
- except Exception as e_hist:
389
- logger.error(f"Failed to store WhatsApp AI chat history for {user_profile.get('uid')}: {e_hist}")
390
-
391
  wa_client.send_text_message(mobile_from_user, response_text)
392
-
393
  except Exception as ai_err:
394
  logger.error(f"Direct AI call from webhook failed for query '{original_query}': {ai_err}", exc_info=True)
395
  wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't get a response from our AI assistant at the moment. Please try again later.")
@@ -397,12 +425,12 @@ def process_whatsapp_text_message(user_profile: Dict[str, Any], message_text: st
397
  wa_client.send_text_message(mobile_from_user, "Our AI assistant is currently unavailable via WhatsApp. Please try again later.")
398
 
399
  elif intent == "unknown":
400
- wa_client.send_text_message(mobile_from_user, "I'm not sure I understood that. Can you try rephrasing? You can say 'menu' for options.")
401
- else:
402
- wa_client.send_text_message(mobile_from_user, f"I understood your intent as '{intent.replace('_',' ')}', but I'm still learning how to handle that fully via WhatsApp.")
403
 
404
 
405
- def process_whatsapp_audio_message(user_profile: Dict[str, Any], audio_id: str, mobile_from_user: str): # Changed Dict
406
  logger.info(f"Processing audio (ID: {audio_id}) from UID {user_profile.get('uid')} ({mobile_from_user})")
407
  if not transcriber_aai:
408
  wa_client.send_text_message(mobile_from_user, "Sorry, I cannot process audio messages at the moment as transcription service is not available.")
@@ -416,7 +444,8 @@ def process_whatsapp_audio_message(user_profile: Dict[str, Any], audio_id: str,
416
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
417
  temp_dir = "temp_audio_whatsapp"
418
  os.makedirs(temp_dir, exist_ok=True)
419
- audio_filename = os.path.join(temp_dir, f"{mobile_from_user.replace('+', '')}_{audio_id}_{timestamp}.ogg")
 
420
  downloaded_path = None
421
  try:
422
  downloaded_path = wa_client.download_media(media_url, audio_filename)
@@ -451,11 +480,11 @@ def whatsapp_webhook():
451
  mode = request.args.get("hub.mode")
452
  token = request.args.get("hub.verify_token")
453
  challenge = request.args.get("hub.challenge")
454
- if mode == "subscribe" and token == WHATSAPP_VERIFY_TOKEN:
455
  logger.info("WhatsApp Webhook verification successful!")
456
  return make_response(challenge, 200)
457
  else:
458
- logger.warning(f"WhatsApp Webhook verification failed. Mode: {mode}, Token: {token} (Expected: {WHATSAPP_VERIFY_TOKEN})")
459
  return make_response("Verification failed", 403)
460
 
461
  if request.method == "POST":
@@ -520,14 +549,16 @@ def whatsapp_webhook():
520
  if button_id:
521
  handle_user_action_confirmation(user_profile, button_id, mobile_from_user)
522
  else: logger.warning(f"Button reply from {mobile_from_user} missing button_id.")
523
- else: logger.info(f"Unhandled interactive type '{interactive_type}' from {mobile_from_user}.")
 
 
524
  else:
525
  logger.info(f"Received unhandled message type '{message_type}' from {mobile_from_user}.")
526
- wa_client.send_text_message(mobile_from_user, "Sorry, I can only process text and voice messages right now.")
527
 
528
- except Exception as e_proc:
529
  logger.error(f"Error processing message for {mobile_from_user} (UID {user_profile.get('uid')}): {e_proc}", exc_info=True)
530
- wa_client.send_text_message(mobile_from_user, "Sorry, an unexpected error occurred while handling your request.")
531
 
532
  return make_response("ok", 200)
533
 
@@ -535,7 +566,7 @@ def whatsapp_webhook():
535
 
536
 
537
  if __name__ == '__main__':
538
- port = int(os.getenv("WHATSAPP_WEBHOOK_PORT", 7860))
539
  debug_mode = os.getenv("FLASK_DEBUG_WHATSAPP", "False").lower() == "true"
540
 
541
  logger.info(f"Starting Tunasonga WhatsApp Webhook. Debug: {debug_mode}, Port: {port}")
@@ -543,16 +574,16 @@ if __name__ == '__main__':
543
  if not FIREBASE_WH_INITIALIZED:
544
  logger.critical("Webhook: Firebase client failed to initialize. Application cannot run reliably.")
545
 
546
- if not WHATSAPP_VERIFY_TOKEN or WHATSAPP_VERIFY_TOKEN == "your_strong_verify_token_here":
547
- logger.critical("CRITICAL: WHATSAPP_VERIFY_TOKEN is not set or is using an insecure default placeholder!")
548
 
549
  if not wa_client.WHATSAPP_TOKEN or not wa_client.PHONE_NUMBER_ID:
550
- logger.critical("CRITICAL: WhatsApp client environment variables (whatsapp_token, phone_number_id) are not set for whatsapp_client.py.")
551
 
552
- if not debug_mode and os.getenv("ENV_MODE", "").lower() == "production":
553
  from waitress import serve
554
  logger.info("Webhook running in production mode using Waitress.")
555
- serve(app, host="0.0.0.0", port=port, threads=int(os.getenv("WAITRESS_THREADS", 8)))
556
  else:
557
  logger.info("Webhook running in debug mode using Flask development server.")
558
  app.run(debug=debug_mode, host="0.0.0.0", port=port)
 
3
  import logging
4
  import json
5
  from dotenv import load_dotenv
6
+ from datetime import datetime, timedelta, timezone
7
  from time import time
8
  import threading
9
  from collections import OrderedDict
10
  import uuid
11
  import re
12
+ from typing import Optional, Dict, Any, List
13
 
 
14
  import utility_tunasonga as tunasonga_utils
 
 
15
  import whatsapp_client as wa_client
16
 
 
17
  from firebase_admin import credentials, db, initialize_app, get_app, exceptions as firebase_exceptions
18
  import firebase_admin
19
 
 
20
  import assemblyai as aai
21
 
22
+ load_dotenv()
23
  app = Flask(__name__)
24
 
25
  logging.basicConfig(
 
41
  cred_wh = credentials.Certificate(credentials_json_wh)
42
  app_name_wh = "TunasongaWhatsAppWebhookApp"
43
 
44
+ already_initialized = False
45
  if firebase_admin._apps:
46
+ for app_key in firebase_admin._apps:
47
+ # Check if an app with this name already exists
48
+ try:
49
+ if firebase_admin.get_app(name=app_key).name == app_name_wh: # Check by name
50
+ firebase_app_instance = firebase_admin.get_app(name=app_name_wh)
51
+ already_initialized = True
52
+ break
53
+ except ValueError: # App with app_key doesn't exist, or other name
54
+ pass
55
 
56
+ if not already_initialized:
57
  firebase_app_instance = initialize_app(cred_wh, {'databaseURL': FIREBASE_DB_URL_WH}, name=app_name_wh)
 
 
58
 
59
  FIREBASE_WH_INITIALIZED = True
60
  logger.info(f"Webhook: Firebase Admin SDK initialized successfully (App: {firebase_app_instance.name}).")
 
65
 
66
  # --- Initialize Utility Modules with necessary configs ---
67
  if FIREBASE_WH_INITIALIZED:
68
+ tunasonga_utils.initialize_gemini_for_utility(
69
+ os.getenv('GOOGLE_API_KEY'),
70
+ model_name="gemini-2.0-flash-001" # Using your specified model name
71
+ )
72
  else:
73
  logger.critical("Webhook: Firebase not initialized, utility functions requiring DB or AI may fail. AI parsing will be disabled.")
74
  tunasonga_utils.GEMINI_LLM_CLIENT = None
75
 
 
76
  # --- WhatsApp & Other Configurations ---
77
+ # Using your original VERIFY_TOKEN value
78
+ VERIFY_TOKEN = os.environ.get("VERIFY_TOKEN", "30cca545-3838-48b2-80a7-9e43b1ae8ce4")
79
+ if not VERIFY_TOKEN or VERIFY_TOKEN == "30cca545-3838-48b2-80a7-9e43b1ae8ce4": # Check if it's the default placeholder
80
+ logger.critical("CRITICAL: VERIFY_TOKEN is not set or is using an insecure default placeholder in main.py!")
81
+
82
 
83
  AAI_API_KEY = os.getenv("AAI_KEY")
84
  transcriber_aai = None
 
92
  else:
93
  logger.warning("Webhook: AAI_KEY not found. Audio transcription disabled for webhook.")
94
 
95
+ # --- Duplicate Message Handling (Sanitized for RTDB) ---
96
  class MessageDeduplicator:
97
  def __init__(self, ttl_hours=24, max_cache_size=10000, db_app_instance_for_dedup=None):
98
  self.ttl_seconds = ttl_hours * 3600
 
104
  if self.db_app_instance:
105
  self._cleanup_thread.start()
106
  else:
107
+ logger.warning("MessageDeduplicator: DB app instance not provided, persistence for processed message IDs disabled. Deduplication will be in-memory only.")
108
+
109
+ def _sanitize_message_id_for_rtdb(self, message_id: str) -> str:
110
+ sanitized = message_id.replace('.', '_').replace('#', '_').replace('$', '_').replace('[', '_').replace(']', '_').replace('/', '_')
111
+ return sanitized
112
 
113
  def is_duplicate(self, message_id: str) -> bool:
114
  if not message_id: return False
115
+ sanitized_message_id = self._sanitize_message_id_for_rtdb(message_id)
116
  with self.lock:
117
+ if sanitized_message_id in self.cache:
118
+ self.cache.move_to_end(sanitized_message_id)
119
  return True
120
  if self.db_app_instance:
121
  try:
122
+ doc_ref = db.reference(f'whatsapp_processed_messages/{sanitized_message_id}', app=self.db_app_instance)
123
  doc = doc_ref.get()
124
  if doc:
125
+ self.cache[sanitized_message_id] = time()
126
  if len(self.cache) > self.max_cache_size: self.cache.popitem(last=False)
127
  return True
128
+ except firebase_exceptions.FirebaseError as fe: logger.error(f"Deduplicator DB error (is_duplicate) for {sanitized_message_id}: {fe}")
129
+ except Exception as e: logger.error(f"Deduplicator generic error (is_duplicate) for {sanitized_message_id}: {e}")
130
  self._mark_processed(message_id)
131
  return False
132
 
133
  def _mark_processed(self, message_id: str):
134
  current_time = time()
135
+ sanitized_message_id = self._sanitize_message_id_for_rtdb(message_id)
136
  with self.lock:
137
+ self.cache[sanitized_message_id] = current_time
138
  if len(self.cache) > self.max_cache_size: self.cache.popitem(last=False)
139
  if self.db_app_instance:
140
  try:
141
+ doc_ref = db.reference(f'whatsapp_processed_messages/{sanitized_message_id}', app=self.db_app_instance)
142
+ doc_ref.set({'original_wamid': message_id, 'timestamp': current_time, 'processed_at_iso': datetime.now(timezone.utc).isoformat()})
143
+ except firebase_exceptions.FirebaseError as fe: logger.error(f"Deduplicator DB error (_mark_processed) for {sanitized_message_id}: {fe}")
144
+ except Exception as e: logger.error(f"Deduplicator generic error (_mark_processed) for {sanitized_message_id}: {e}")
145
+
146
  def _periodic_cleanup(self):
147
  while True:
148
  try:
 
155
  for msg_id in expired_ids_mem:
156
  self.cache.pop(msg_id, None)
157
  if expired_ids_mem: logger.info(f"Deduplicator: Cleaned {len(expired_ids_mem)} expired IDs from in-memory cache.")
158
+
159
+ if self.db_app_instance and os.getenv("ENABLE_DEDUPLICATOR_DB_CLEANUP", "false").lower() == "true":
160
+ try:
161
+ cutoff_timestamp = current_time - self.ttl_seconds
162
+ processed_ref = db.reference('whatsapp_processed_messages', app=self.db_app_instance)
163
+ old_messages_query = processed_ref.order_by_child('timestamp').end_at(cutoff_timestamp)
164
+ old_messages = old_messages_query.get()
165
+ if old_messages:
166
+ delete_count = 0
167
+ for msg_key in old_messages.keys():
168
+ processed_ref.child(msg_key).delete()
169
+ delete_count +=1
170
+ if delete_count > 0: logger.info(f"Deduplicator: Cleaned {delete_count} expired message IDs from Firebase RTDB.")
171
+ except Exception as db_cleanup_err: logger.error(f"Deduplicator: Error during DB cleanup: {db_cleanup_err}", exc_info=True)
172
  threading.Event().wait(3600)
173
  except Exception as e:
174
  logger.error(f"Error in Deduplicator cleanup thread: {e}", exc_info=True)
 
186
  return message_deduplicator.is_duplicate(message_id)
187
 
188
  # --- Core Webhook Logic ---
 
189
  def handle_user_action_confirmation(user_profile: Dict[str, Any], action_id_from_button: str, mobile_from_user: str):
190
  if not FIREBASE_WH_INITIALIZED:
191
  wa_client.send_text_message(mobile_from_user, "Sorry, our system is temporarily unable to process confirmations.")
 
193
 
194
  try:
195
  parts = action_id_from_button.split('_', 2)
196
+ if len(parts) < 3:
197
+ logger.warning(f"Invalid button ID format: {action_id_from_button} from {mobile_from_user}")
198
+ wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't understand that button press.")
199
+ return
200
+
201
+ confirm_or_cancel = parts[0]
202
+ # action_type_from_button_raw = parts[1] # Example: "action"
203
  pending_action_id = parts[2]
204
  except IndexError:
205
+ logger.warning(f"Invalid button ID format (IndexError): {action_id_from_button} from {mobile_from_user}")
206
  wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't understand that button press.")
207
  return
208
 
209
+ pending_action_retrieved = tunasonga_utils.retrieve_pending_action(mobile_from_user, pending_action_id, firebase_app_instance)
210
 
211
+ if not pending_action_retrieved:
212
  wa_client.send_text_message(mobile_from_user, "It seems this action has expired or was already processed. Please try again if needed.")
213
  return
214
 
215
+ original_parsed_llm_response = pending_action_retrieved.get("data", {})
216
+ stored_intent = original_parsed_llm_response.get("intent", "unknown")
217
+ stored_entities = original_parsed_llm_response.get("entities", {})
218
 
219
  if confirm_or_cancel == "confirm":
220
+ logger.info(f"User {mobile_from_user} (UID: {user_profile.get('uid')}) confirmed action: {stored_intent} with pending ID {pending_action_id}")
 
221
  response_message = "Action confirmed and processed!"
222
 
223
+ if stored_intent == "list_produce":
224
  listing_id = str(uuid.uuid4())
225
  listing_payload = {
226
+ 'lister_id': user_profile.get('uid'), 'listing_type': 'produce',
227
+ 'status': 'pending_approval', 'created_at': datetime.now(timezone.utc).isoformat(),
228
+ 'whatsapp_source': True, 'crop_type': stored_entities.get('crop_type'),
229
+ 'quantity': stored_entities.get('quantity'), 'unit': stored_entities.get('unit'),
230
+ 'asking_price': stored_entities.get('price') if stored_entities.get('price_type') == 'per_unit' else None,
231
+ 'total_asking_price': stored_entities.get('price') if stored_entities.get('price_type') == 'total' else None,
232
+ 'currency': stored_entities.get('currency'),
233
+ 'location': stored_entities.get('location') or user_profile.get('location', 'Unknown'),
234
+ 'availability_description': stored_entities.get('availability_description'),
235
+ 'quality_description': stored_entities.get('quality_description')
 
236
  }
237
  listing_payload_cleaned = {k: v for k, v in listing_payload.items() if v is not None}
238
  try:
239
  db.reference(f'listings/{listing_id}', app=firebase_app_instance).set(listing_payload_cleaned)
240
+ response_message = f"Great! Your produce listing for {stored_entities.get('quantity')} {stored_entities.get('unit','')} of {stored_entities.get('crop_type')} has been submitted. Admin may review it."
241
  except Exception as e_db:
242
  logger.error(f"DB error creating listing for {mobile_from_user}: {e_db}", exc_info=True)
243
  response_message = "Sorry, there was an error submitting your listing. Please try again."
244
 
245
+ elif stored_intent == "post_demand":
246
  demand_id = str(uuid.uuid4())
247
  demand_payload = {
248
  'lister_id': user_profile.get('uid'), 'listing_type': 'demand',
249
  'status': 'pending_approval', 'created_at': datetime.now(timezone.utc).isoformat(),
250
+ 'whatsapp_source': True, 'crop_type': stored_entities.get('crop_type'),
251
+ 'quantity': stored_entities.get('quantity'), 'unit': stored_entities.get('unit'),
252
+ 'price_range': f"{stored_entities.get('currency','')}{stored_entities.get('price')} ({stored_entities.get('price_type')})" if stored_entities.get('price') else "Not specified",
253
+ 'location': stored_entities.get('location') or user_profile.get('location', 'Unknown'),
254
+ 'needed_by_description': stored_entities.get('availability_description'),
255
+ 'quality_specs': stored_entities.get('quality_description')
256
  }
257
  demand_payload_cleaned = {k: v for k, v in demand_payload.items() if v is not None}
258
  try:
259
  db.reference(f'listings/{demand_id}', app=firebase_app_instance).set(demand_payload_cleaned)
260
+ response_message = f"Okay! Your demand for {stored_entities.get('quantity')} {stored_entities.get('unit','')} of {stored_entities.get('crop_type')} has been posted. Admin may review it."
261
  except Exception as e_db:
262
  logger.error(f"DB error creating demand for {mobile_from_user}: {e_db}", exc_info=True)
263
  response_message = "Sorry, there was an error posting your demand. Please try again."
264
  else:
265
+ response_message = f"Confirmed! However, I'm not sure how to process action type '{stored_intent}' yet from confirmation."
266
+
267
  wa_client.send_text_message(mobile_from_user, response_message)
268
 
269
  elif confirm_or_cancel == "cancel":
270
+ logger.info(f"User {mobile_from_user} cancelled action: {stored_intent} with pending ID {pending_action_id}")
271
  wa_client.send_text_message(mobile_from_user, "Okay, I've cancelled that action. Let me know if there's anything else!")
272
  else:
273
  logger.warning(f"Unknown confirmation command '{confirm_or_cancel}' for action ID {pending_action_id} from {mobile_from_user}")
274
  wa_client.send_text_message(mobile_from_user, "Sorry, I didn't understand that confirmation.")
275
+
276
  tunasonga_utils.delete_pending_action(mobile_from_user, pending_action_id, firebase_app_instance)
277
 
278
 
279
  def process_whatsapp_text_message(user_profile: Dict[str, Any], message_text: str, mobile_from_user: str):
280
+ user_name_for_prompt = user_profile.get('name', 'User')
281
+ logger.info(f"Processing text from UID {user_profile.get('uid')} ({user_name_for_prompt} - {mobile_from_user}): '{message_text}'")
282
 
283
+ greeting_match = re.match(r'^\s*(hi|hello|menu|help|options|sawubona|mhoro|salibonani)\b.*$', message_text, re.IGNORECASE)
284
+ if greeting_match:
285
  wa_client.send_text_message(mobile_from_user,
286
+ f"Hi {user_name_for_prompt}! Welcome to Tunasonga Agri WhatsApp.\n"
287
  "You can:\n"
288
+ "🌾 List produce: e.g., 'Sell 10 bags maize for $100 total'\n"
289
+ "🛒 Post demand: e.g., 'Need 50kg tomatoes in Harare'\n"
290
  "📋 Check your listings: 'My listings'\n"
291
  "🤝 Check your deals: 'My deals'\n"
292
+ "🌱 Ask for farming advice: e.g., 'How to store potatoes?'\n"
293
+ "📈 Ask about market trends: e.g., 'Maize price trends?'"
294
  )
295
  return
296
 
297
+ parsed_llm_response = tunasonga_utils.parse_user_request_with_gemini(message_text)
298
 
299
  if not parsed_llm_response or "error" in parsed_llm_response or not tunasonga_utils.GEMINI_LLM_CLIENT:
300
  error_detail = parsed_llm_response.get("error", "Could not understand request") if parsed_llm_response else "Could not understand request"
301
  if not tunasonga_utils.GEMINI_LLM_CLIENT: error_detail = "AI parsing service unavailable."
302
+ logger.warning(f"LLM parsing failed or unavailable for '{message_text}' from {mobile_from_user}. Error: {error_detail}")
303
+ wa_client.send_text_message(mobile_from_user, f"Sorry, I had trouble understanding that. ({error_detail}). Could you try rephrasing clearly, or say 'menu' for options?")
304
  return
305
 
306
  intent = parsed_llm_response.get("intent", "unknown")
307
  entities = parsed_llm_response.get("entities", {})
308
+ logger.info(f"LLM parsed intent: {intent}, entities: {json.dumps(entities)} for {mobile_from_user}")
309
 
310
  if intent == "list_produce" or intent == "post_demand":
311
  crop = entities.get("crop_type")
312
  qty = entities.get("quantity")
313
+
314
  if not crop or qty is None:
315
  missing_info = []
316
+ if not crop: missing_info.append("the crop type")
317
+ if qty is None: missing_info.append("the quantity")
318
+ wa_client.send_text_message(mobile_from_user, f"To {intent.replace('_', ' ')}, I need at least {', and '.join(missing_info)}. Can you please provide that information?")
319
  return
320
+
321
  pending_action_id = str(uuid.uuid4())
322
+ if tunasonga_utils.persist_pending_action(mobile_from_user, pending_action_id, parsed_llm_response, firebase_app_instance):
 
 
323
  summary = f"Action: {intent.replace('_',' ').title()}\n"
324
  summary += f"- Crop: {entities.get('crop_type', 'N/A')}\n"
325
  summary += f"- Quantity: {entities.get('quantity', 'N/A')} {entities.get('unit', '')}\n"
 
328
  if entities.get('location'): summary += f"- Location: {entities.get('location')}\n"
329
  if entities.get('availability_description'): summary += f"- Availability: {entities.get('availability_description')}\n"
330
  if entities.get('quality_description'): summary += f"- Quality: {entities.get('quality_description')}\n"
331
+
332
  buttons = [
333
+ {"reply": {"id": f"confirm_action_{pending_action_id}", "title": "✅ Yes, Proceed"}},
334
+ {"reply": {"id": f"cancel_action_{pending_action_id}", "title": "❌ No, Cancel"}}
335
  ]
336
+ body_text = f"Okay, {user_name_for_prompt}, I understand you want to:\n\n{summary.strip()}\n\nIs this correct?"
337
  wa_client.send_reply_buttons(mobile_from_user, body_text, buttons)
338
  else:
339
+ wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't save your request for confirmation at the moment. Please try again.")
340
 
341
  elif intent == "get_my_listings":
342
+ user_uid_for_query = user_profile.get('uid')
343
+ if not user_uid_for_query:
344
+ wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't identify your account to fetch listings.")
345
+ return
346
  listings_ref = db.reference('listings', app=firebase_app_instance)\
347
  .order_by_child('lister_id')\
348
+ .equal_to(user_uid_for_query)\
349
  .get()
350
  active_listings = {lid: ldata for lid, ldata in (listings_ref or {}).items() if isinstance(ldata, dict) and ldata.get('status') != 'closed'}
351
+ response_text = tunasonga_utils.format_listings_for_whatsapp(active_listings, "listings")
352
  wa_client.send_text_message(mobile_from_user, response_text)
353
 
354
  elif intent == "get_my_deals":
355
+ user_uid_for_query = user_profile.get('uid')
356
+ if not user_uid_for_query:
357
+ wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't identify your account to fetch deals.")
358
+ return
359
  all_deals = db.reference('deals', app=firebase_app_instance).get() or {}
360
  my_deals = {}
 
361
  for deal_id, deal_data in all_deals.items():
362
  if isinstance(deal_data, dict) and \
363
+ (deal_data.get('buyer_id') == user_uid_for_query or \
364
+ deal_data.get('farmer_id') == user_uid_for_query or \
365
+ deal_data.get('assigned_transporter_id') == user_uid_for_query):
366
  my_deals[deal_id] = deal_data
367
+ response_text = tunasonga_utils.format_deals_for_whatsapp(my_deals, user_uid_for_query, firebase_app_instance)
368
  wa_client.send_text_message(mobile_from_user, response_text)
369
 
370
  elif intent == "get_platform_demands" or intent == "get_platform_produce":
 
372
  query_ref = db.reference('listings', app=firebase_app_instance)\
373
  .order_by_child('listing_type')\
374
  .equal_to(listing_type_filter)
375
+ all_ofType = query_ref.limit_to_first(10).get() or {}
376
+
377
  filtered_listings = {}
378
  target_crop = entities.get("crop_type", "").lower() if entities.get("crop_type") else None
379
+
380
  for lid, ldata in all_ofType.items():
381
  if isinstance(ldata, dict) and ldata.get('status') == 'active':
382
  if target_crop:
383
  if target_crop in ldata.get("crop_type", "").lower():
384
  filtered_listings[lid] = ldata
385
+ else:
386
+ filtered_listings[lid] = ldata
387
+
388
  if not filtered_listings:
389
+ response_text = f"I couldn't find any active {listing_type_filter} listings right now"
390
  if target_crop: response_text += f" for {entities.get('crop_type')}"
391
+ response_text += ". You can try a broader search or check back later."
392
  else:
393
+ response_text = f"Here are some active {listing_type_filter} listings"
394
  if target_crop: response_text += f" for {entities.get('crop_type')}"
395
+ response_text += " (showing up to 5):\n"
396
+ response_text += tunasonga_utils.format_listings_for_whatsapp(dict(list(filtered_listings.items())[:5]), listing_type_filter)
397
+ if len(filtered_listings) > 5: response_text += "\n...and more. For a full view, please use our app or website, or refine your search."
398
  wa_client.send_text_message(mobile_from_user, response_text)
399
 
400
  elif intent == "ai_chat_query":
401
  original_query = entities.get("original_query_for_ai_chat", message_text)
402
+ logger.info(f"Webhook: Handling direct AI chat query for {mobile_from_user} (UID: {user_profile.get('uid')}, Name: {user_name_for_prompt}): '{original_query}'")
403
 
404
+ if tunasonga_utils.GEMINI_LLM_CLIENT:
405
  try:
406
+ ai_chat_system_prompt = f"You are Tunasonga Agri Assistant, an expert in agriculture in the SADC region, focusing on smallholder farmers. The user you are talking to is named {user_name_for_prompt}. Provide a helpful, concise, and practical answer to their question. If the question is outside of agriculture, farming, or agri-business in the SADC context, politely state that you can primarily assist with agricultural topics."
407
+ full_chat_prompt = f"{ai_chat_system_prompt}\n\nUser ({user_name_for_prompt}) asks: \"{original_query}\"\n\nAnswer:"
 
 
 
 
 
 
 
 
 
408
 
409
  gemini_response = tunasonga_utils.GEMINI_LLM_CLIENT.generate_content(full_chat_prompt)
410
 
411
  response_text = gemini_response.text.strip() if gemini_response.text else "I received an empty response from the AI assistant. Could you try asking in a different way?"
412
 
413
+ try:
 
414
  db.reference(f'ai_chat_history/{user_profile.get("uid")}/{str(uuid.uuid4())}', app=firebase_app_instance).set({
415
+ 'user_message': original_query, 'ai_response': response_text,
 
416
  'intent_classified': 'whatsapp_direct_ai_query',
417
+ 'timestamp': datetime.now(timezone.utc).isoformat(), 'source': 'whatsapp'
 
418
  })
419
+ except Exception as e_hist: logger.error(f"Failed to store WhatsApp AI chat history for {user_profile.get('uid')}: {e_hist}")
 
 
420
  wa_client.send_text_message(mobile_from_user, response_text)
 
421
  except Exception as ai_err:
422
  logger.error(f"Direct AI call from webhook failed for query '{original_query}': {ai_err}", exc_info=True)
423
  wa_client.send_text_message(mobile_from_user, "Sorry, I couldn't get a response from our AI assistant at the moment. Please try again later.")
 
425
  wa_client.send_text_message(mobile_from_user, "Our AI assistant is currently unavailable via WhatsApp. Please try again later.")
426
 
427
  elif intent == "unknown":
428
+ wa_client.send_text_message(mobile_from_user, f"I'm not sure I understood that, {user_name_for_prompt}. Can you try rephrasing? You can say 'menu' for options.")
429
+ else:
430
+ wa_client.send_text_message(mobile_from_user, f"I understood your intent as '{intent.replace('_',' ')}'. I'm still learning how to handle that fully via WhatsApp. Try saying 'menu' for current options.")
431
 
432
 
433
+ def process_whatsapp_audio_message(user_profile: Dict[str, Any], audio_id: str, mobile_from_user: str):
434
  logger.info(f"Processing audio (ID: {audio_id}) from UID {user_profile.get('uid')} ({mobile_from_user})")
435
  if not transcriber_aai:
436
  wa_client.send_text_message(mobile_from_user, "Sorry, I cannot process audio messages at the moment as transcription service is not available.")
 
444
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
445
  temp_dir = "temp_audio_whatsapp"
446
  os.makedirs(temp_dir, exist_ok=True)
447
+ safe_audio_id = re.sub(r'[^\w\-.]', '_', audio_id)
448
+ audio_filename = os.path.join(temp_dir, f"{mobile_from_user.replace('+', '')}_{safe_audio_id}_{timestamp}.ogg")
449
  downloaded_path = None
450
  try:
451
  downloaded_path = wa_client.download_media(media_url, audio_filename)
 
480
  mode = request.args.get("hub.mode")
481
  token = request.args.get("hub.verify_token")
482
  challenge = request.args.get("hub.challenge")
483
+ if mode == "subscribe" and token == VERIFY_TOKEN: # Use VERIFY_TOKEN from top of file
484
  logger.info("WhatsApp Webhook verification successful!")
485
  return make_response(challenge, 200)
486
  else:
487
+ logger.warning(f"WhatsApp Webhook verification failed. Mode: {mode}, Token: {token} (Expected: {VERIFY_TOKEN})")
488
  return make_response("Verification failed", 403)
489
 
490
  if request.method == "POST":
 
549
  if button_id:
550
  handle_user_action_confirmation(user_profile, button_id, mobile_from_user)
551
  else: logger.warning(f"Button reply from {mobile_from_user} missing button_id.")
552
+ else:
553
+ logger.info(f"Unhandled interactive type '{interactive_type}' from {mobile_from_user}.")
554
+ wa_client.send_text_message(mobile_from_user, "I received your button press, but I'm not sure how to handle that specific type yet.")
555
  else:
556
  logger.info(f"Received unhandled message type '{message_type}' from {mobile_from_user}.")
557
+ wa_client.send_text_message(mobile_from_user, "Sorry, I can only process text and voice messages right now. Please try sending your request as text or a voice note.")
558
 
559
+ except Exception as e_proc:
560
  logger.error(f"Error processing message for {mobile_from_user} (UID {user_profile.get('uid')}): {e_proc}", exc_info=True)
561
+ wa_client.send_text_message(mobile_from_user, "Sorry, an unexpected error occurred while handling your request. Please try again.")
562
 
563
  return make_response("ok", 200)
564
 
 
566
 
567
 
568
  if __name__ == '__main__':
569
+ port = int(os.getenv("WHATSAPP_WEBHOOK_PORT", 7860)) # Using your original port
570
  debug_mode = os.getenv("FLASK_DEBUG_WHATSAPP", "False").lower() == "true"
571
 
572
  logger.info(f"Starting Tunasonga WhatsApp Webhook. Debug: {debug_mode}, Port: {port}")
 
574
  if not FIREBASE_WH_INITIALIZED:
575
  logger.critical("Webhook: Firebase client failed to initialize. Application cannot run reliably.")
576
 
577
+ if not VERIFY_TOKEN or VERIFY_TOKEN == "30cca545-3838-48b2-80a7-9e43b1ae8ce4":
578
+ logger.critical("CRITICAL: VERIFY_TOKEN (main.py) is not set or is using an insecure default placeholder!")
579
 
580
  if not wa_client.WHATSAPP_TOKEN or not wa_client.PHONE_NUMBER_ID:
581
+ logger.critical("CRITICAL: WhatsApp client environment variables (whatsapp_token, phone_number_id) are not set for whatsapp_client.py. Outgoing messages will fail.")
582
 
583
+ if not debug_mode and os.getenv("ENV_MODE", "development").lower() == "production":
584
  from waitress import serve
585
  logger.info("Webhook running in production mode using Waitress.")
586
+ serve(app, host="0.0.0.0", port=port, threads=int(os.getenv("WAITRESS_THREADS_WH", 4)))
587
  else:
588
  logger.info("Webhook running in debug mode using Flask development server.")
589
  app.run(debug=debug_mode, host="0.0.0.0", port=port)