Spaces:
Running
Running
Chandima Prabhath
commited on
Commit
Β·
21738c6
1
Parent(s):
00f979e
update debugs
Browse files
app.py
CHANGED
@@ -4,6 +4,7 @@ Author: Assistant
|
|
4 |
Description: A comprehensive WhatsApp bot with a professional, class-based structure.
|
5 |
Features include image generation, image editing, voice replies,
|
6 |
and various utility functions, all handled by an asynchronous task queue.
|
|
|
7 |
"""
|
8 |
|
9 |
import os
|
@@ -13,12 +14,12 @@ import logging
|
|
13 |
import queue
|
14 |
import json
|
15 |
import base64
|
16 |
-
from typing import List, Optional, Union, Literal, Dict, Any, Tuple
|
17 |
from collections import defaultdict, deque
|
18 |
from concurrent.futures import ThreadPoolExecutor
|
19 |
|
20 |
from fastapi import FastAPI, Request, HTTPException
|
21 |
-
from fastapi.responses import JSONResponse
|
22 |
from pydantic import BaseModel, Field, ValidationError
|
23 |
import uvicorn
|
24 |
|
@@ -43,14 +44,20 @@ class BotConfig:
|
|
43 |
DEFAULT_IMAGE_COUNT: int = 4
|
44 |
MAX_HISTORY_SIZE: int = 20
|
45 |
WORKER_THREADS: int = 4
|
46 |
-
|
|
|
|
|
|
|
|
|
47 |
|
48 |
def __init__(self):
|
|
|
49 |
self.GREEN_API_URL = os.getenv("GREEN_API_URL")
|
50 |
self.GREEN_API_TOKEN = os.getenv("GREEN_API_TOKEN")
|
51 |
self.GREEN_API_ID_INSTANCE = os.getenv("GREEN_API_ID_INSTANCE")
|
52 |
self.WEBHOOK_AUTH_TOKEN = os.getenv("WEBHOOK_AUTH_TOKEN")
|
53 |
-
|
|
|
54 |
self._validate()
|
55 |
|
56 |
def _validate(self):
|
@@ -69,19 +76,31 @@ class LoggerSetup:
|
|
69 |
"""Sets up and manages structured logging for the application."""
|
70 |
@staticmethod
|
71 |
def setup(level: str) -> logging.Logger:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
logger = logging.getLogger("whatsapp_bot")
|
73 |
logger.setLevel(level)
|
74 |
-
|
|
|
|
|
|
|
75 |
|
76 |
handler = logging.StreamHandler()
|
|
|
77 |
formatter = logging.Formatter(
|
78 |
-
"%(asctime)s [%(levelname)s] [%(chat_id)s] %(funcName)s:%(lineno)d - %(message)s"
|
79 |
)
|
80 |
handler.setFormatter(formatter)
|
81 |
|
82 |
class ContextFilter(logging.Filter):
|
|
|
83 |
def filter(self, record):
|
84 |
-
record.chat_id = ThreadContext.get_context().get("chat_id", "
|
85 |
return True
|
86 |
|
87 |
handler.addFilter(ContextFilter())
|
@@ -96,11 +115,13 @@ class ThreadContext:
|
|
96 |
|
97 |
@classmethod
|
98 |
def set_context(cls, chat_id: str, message_id: str):
|
|
|
99 |
cls._context.chat_id = chat_id
|
100 |
cls._context.message_id = message_id
|
101 |
|
102 |
@classmethod
|
103 |
def get_context(cls) -> Dict[str, Optional[str]]:
|
|
|
104 |
return {
|
105 |
"chat_id": getattr(cls._context, "chat_id", None),
|
106 |
"message_id": getattr(cls._context, "message_id", None),
|
@@ -143,6 +164,7 @@ class GreenApiClient:
|
|
143 |
url = f"{self.base_url}/{endpoint}/{self.config.GREEN_API_TOKEN}"
|
144 |
for attempt in range(3):
|
145 |
try:
|
|
|
146 |
response = self.session.request(method, url, timeout=20, **kwargs)
|
147 |
response.raise_for_status()
|
148 |
return response.json()
|
@@ -224,6 +246,7 @@ class IntentRouter:
|
|
224 |
try:
|
225 |
raw_response = generate_llm(system_prompt)
|
226 |
except LLMBadRequestError:
|
|
|
227 |
self.conv_manager.clear_history(chat_id)
|
228 |
return SendTextIntent(action="send_text", message="Oops! Let's start fresh! π")
|
229 |
|
@@ -252,14 +275,16 @@ class IntentRouter:
|
|
252 |
|
253 |
def _parse_response(self, raw_response: str) -> BaseIntent:
|
254 |
try:
|
255 |
-
|
|
|
|
|
256 |
for model in self.INTENT_MODELS:
|
257 |
try:
|
258 |
return model.model_validate(parsed)
|
259 |
except ValidationError:
|
260 |
continue
|
261 |
except json.JSONDecodeError:
|
262 |
-
|
263 |
|
264 |
# Fallback for non-JSON or unparsable responses
|
265 |
return SendTextIntent(action="send_text", message=raw_response)
|
@@ -282,14 +307,22 @@ class WhatsAppBot:
|
|
282 |
@self.fastapi_app.post("/whatsapp")
|
283 |
async def webhook(request: Request):
|
284 |
if request.headers.get("Authorization") != f"Bearer {self.config.WEBHOOK_AUTH_TOKEN}":
|
|
|
285 |
raise HTTPException(403, "Unauthorized")
|
286 |
|
287 |
-
|
288 |
-
|
|
|
|
|
289 |
|
290 |
-
|
291 |
-
|
292 |
-
|
|
|
|
|
|
|
|
|
|
|
293 |
|
294 |
return JSONResponse(content={"status": "received"})
|
295 |
|
@@ -307,13 +340,16 @@ class WhatsAppBot:
|
|
307 |
while True:
|
308 |
task = self.task_queue.get()
|
309 |
try:
|
|
|
|
|
310 |
handler = getattr(self, f"_task_{task['type']}", None)
|
311 |
if handler:
|
|
|
312 |
handler(task)
|
313 |
else:
|
314 |
-
self.logger.warning(f"Unknown task type: {task['type']}")
|
315 |
except Exception as e:
|
316 |
-
self.logger.error(f"Error processing task {task
|
317 |
finally:
|
318 |
self.task_queue.task_done()
|
319 |
|
@@ -322,6 +358,14 @@ class WhatsAppBot:
|
|
322 |
try:
|
323 |
chat_id = payload["senderData"]["chatId"]
|
324 |
message_id = payload["idMessage"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
325 |
ThreadContext.set_context(chat_id, message_id)
|
326 |
|
327 |
message_data = payload.get("messageData", {})
|
@@ -335,8 +379,10 @@ class WhatsAppBot:
|
|
335 |
|
336 |
text = text.strip()
|
337 |
if not text:
|
|
|
338 |
return
|
339 |
|
|
|
340 |
self.conv_manager.add_user_message(chat_id, text)
|
341 |
|
342 |
# Handle direct commands
|
@@ -346,6 +392,8 @@ class WhatsAppBot:
|
|
346 |
# Handle natural language and replies
|
347 |
self._handle_natural_language(chat_id, message_id, text, payload)
|
348 |
|
|
|
|
|
349 |
except Exception as e:
|
350 |
self.logger.error(f"Failed to process message payload: {e}", exc_info=True)
|
351 |
|
@@ -372,11 +420,11 @@ class WhatsAppBot:
|
|
372 |
elif command == "/edit":
|
373 |
self._dispatch_edit_image(chat_id, message_id, args, payload)
|
374 |
elif command == "/joke":
|
375 |
-
self.
|
376 |
elif command == "/inspire":
|
377 |
-
self.
|
378 |
elif command == "/weather":
|
379 |
-
self.
|
380 |
else:
|
381 |
self.api_client.send_message(chat_id, "Unknown command. Type /help for options.", message_id)
|
382 |
|
@@ -394,6 +442,7 @@ class WhatsAppBot:
|
|
394 |
# This action needs the original payload to find the replied-to image
|
395 |
self._dispatch_edit_image(chat_id, message_id, intent.prompt, payload)
|
396 |
elif hasattr(self, f"_task_{intent.action}"):
|
|
|
397 |
self.task_queue.put({"type": intent.action, **task_data})
|
398 |
else:
|
399 |
self.logger.warning(f"No handler found for intent action: {intent.action}")
|
@@ -401,7 +450,7 @@ class WhatsAppBot:
|
|
401 |
|
402 |
def _dispatch_edit_image(self, chat_id, message_id, prompt, payload):
|
403 |
"""Checks for a replied-to image and dispatches the edit task."""
|
404 |
-
quoted_message = payload.get("messageData", {}).get("quotedMessage")
|
405 |
if not quoted_message or quoted_message.get("typeMessage") != "imageMessage":
|
406 |
self.api_client.send_message(chat_id, "To edit an image, please reply to it with your instructions.", message_id)
|
407 |
return
|
@@ -421,6 +470,7 @@ class WhatsAppBot:
|
|
421 |
chat_id, message_id, message = task["chat_id"], task["message_id"], task["message"]
|
422 |
self.api_client.send_message(chat_id, message, message_id)
|
423 |
self.conv_manager.add_bot_message(chat_id, message)
|
|
|
424 |
self.task_queue.put({"type": "voice_reply", "chat_id": chat_id, "message_id": message_id, "text": message})
|
425 |
|
426 |
def _task_generate_image(self, task: Dict[str, Any]):
|
@@ -434,7 +484,7 @@ class WhatsAppBot:
|
|
434 |
self.api_client.send_file(chat_id, path, caption, mid)
|
435 |
os.remove(path)
|
436 |
except Exception as e:
|
437 |
-
self.logger.error(f"Image generation {i+1} failed: {e}")
|
438 |
self.api_client.send_message(chat_id, f"π’ Failed to generate image {i+1}.", mid)
|
439 |
|
440 |
def _task_edit_image(self, task: Dict[str, Any]):
|
@@ -447,6 +497,7 @@ class WhatsAppBot:
|
|
447 |
if not image_data:
|
448 |
raise ValueError("Failed to download image.")
|
449 |
|
|
|
450 |
input_path = os.path.join(self.config.TEMP_DIR, f"input_{mid}.jpg")
|
451 |
output_path = os.path.join(self.config.TEMP_DIR, f"output_{mid}.jpg")
|
452 |
|
@@ -462,7 +513,7 @@ class WhatsAppBot:
|
|
462 |
raise ValueError("Edited image file not found.")
|
463 |
|
464 |
except Exception as e:
|
465 |
-
self.logger.error(f"Image editing task failed: {e}")
|
466 |
self.api_client.send_message(chat_id, "π’ Sorry, I failed to edit the image.", mid)
|
467 |
finally:
|
468 |
for path in [input_path, output_path]:
|
@@ -479,13 +530,14 @@ class WhatsAppBot:
|
|
479 |
self.api_client.send_file(task["chat_id"], path, quoted_message_id=task["message_id"])
|
480 |
os.remove(path)
|
481 |
except Exception as e:
|
482 |
-
self.logger.warning(f"Voice reply generation failed: {e}")
|
483 |
|
484 |
def _task_joke(self, task: Dict[str, Any]):
|
485 |
try:
|
486 |
j = requests.get("https://official-joke-api.appspot.com/random_joke", timeout=5).json()
|
487 |
joke = f"{j['setup']}\n\n{j['punchline']}"
|
488 |
except Exception:
|
|
|
489 |
joke = generate_llm("Tell me a short, clean joke.")
|
490 |
self._task_send_text({"type": "send_text", **task, "message": f"π {joke}"})
|
491 |
|
@@ -494,13 +546,16 @@ class WhatsAppBot:
|
|
494 |
self._task_send_text({"type": "send_text", **task, "message": f"β¨ {quote}"})
|
495 |
|
496 |
def _task_weather(self, task: Dict[str, Any]):
|
497 |
-
location = task.get("location"
|
|
|
|
|
|
|
498 |
try:
|
499 |
raw = requests.get(f"http://wttr.in/{location.replace(' ', '+')}?format=4", timeout=10).text
|
500 |
report = generate_llm(f"Create a friendly weather report in Celsius from this data:\n\n{raw}")
|
501 |
self._task_send_text({"type": "send_text", **task, "message": f"π€οΈ Weather for {location}:\n{report}"})
|
502 |
except Exception as e:
|
503 |
-
self.logger.error(f"Weather task failed: {e}")
|
504 |
self.api_client.send_message(task["chat_id"], "Sorry, I couldn't get the weather.", task["message_id"])
|
505 |
|
506 |
def run(self):
|
@@ -508,27 +563,29 @@ class WhatsAppBot:
|
|
508 |
self.logger.info("Starting Eve WhatsApp Bot...")
|
509 |
for d in [self.config.IMAGE_DIR, self.config.AUDIO_DIR, self.config.TEMP_DIR]:
|
510 |
os.makedirs(d, exist_ok=True)
|
511 |
-
self.logger.
|
512 |
|
|
|
513 |
self.api_client.send_message(
|
514 | |
515 |
"π Eve is online and ready to help! Type /help to see commands."
|
516 |
)
|
517 |
|
518 |
-
uvicorn.run(self.fastapi_app, host="0.0.0.0", port=7860)
|
519 |
|
520 |
|
521 |
if __name__ == "__main__":
|
522 |
try:
|
523 |
-
|
524 |
-
executor = ThreadPoolExecutor(max_workers=
|
525 |
-
bot = WhatsAppBot(
|
526 |
bot.run()
|
527 |
except ValueError as e:
|
528 |
# Catch config validation errors
|
529 |
-
|
|
|
530 |
except KeyboardInterrupt:
|
531 |
print("\nπ Bot stopped by user.")
|
532 |
except Exception as e:
|
533 |
-
|
534 |
-
|
|
|
4 |
Description: A comprehensive WhatsApp bot with a professional, class-based structure.
|
5 |
Features include image generation, image editing, voice replies,
|
6 |
and various utility functions, all handled by an asynchronous task queue.
|
7 |
+
Includes request logging and chat ID filtering.
|
8 |
"""
|
9 |
|
10 |
import os
|
|
|
14 |
import queue
|
15 |
import json
|
16 |
import base64
|
17 |
+
from typing import List, Optional, Union, Literal, Dict, Any, Tuple, Set
|
18 |
from collections import defaultdict, deque
|
19 |
from concurrent.futures import ThreadPoolExecutor
|
20 |
|
21 |
from fastapi import FastAPI, Request, HTTPException
|
22 |
+
from fastapi.responses import JSONResponse
|
23 |
from pydantic import BaseModel, Field, ValidationError
|
24 |
import uvicorn
|
25 |
|
|
|
44 |
DEFAULT_IMAGE_COUNT: int = 4
|
45 |
MAX_HISTORY_SIZE: int = 20
|
46 |
WORKER_THREADS: int = 4
|
47 |
+
# Set log level to DEBUG to capture all incoming requests
|
48 |
+
LOG_LEVEL: str = "DEBUG"
|
49 |
+
|
50 |
+
# Whitelisted chat IDs. The bot will only respond to these chats.
|
51 |
+
ALLOWED_CHATS: Set[str] = {"[email protected]", "[email protected]"}
|
52 |
|
53 |
def __init__(self):
|
54 |
+
"""Initializes configuration from environment variables."""
|
55 |
self.GREEN_API_URL = os.getenv("GREEN_API_URL")
|
56 |
self.GREEN_API_TOKEN = os.getenv("GREEN_API_TOKEN")
|
57 |
self.GREEN_API_ID_INSTANCE = os.getenv("GREEN_API_ID_INSTANCE")
|
58 |
self.WEBHOOK_AUTH_TOKEN = os.getenv("WEBHOOK_AUTH_TOKEN")
|
59 |
+
# Allow overriding log level from environment
|
60 |
+
self.LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
|
61 |
self._validate()
|
62 |
|
63 |
def _validate(self):
|
|
|
76 |
"""Sets up and manages structured logging for the application."""
|
77 |
@staticmethod
|
78 |
def setup(level: str) -> logging.Logger:
|
79 |
+
"""
|
80 |
+
Configures the root logger for the application.
|
81 |
+
Args:
|
82 |
+
level: The logging level (e.g., "DEBUG", "INFO").
|
83 |
+
Returns:
|
84 |
+
A configured logger instance.
|
85 |
+
"""
|
86 |
logger = logging.getLogger("whatsapp_bot")
|
87 |
logger.setLevel(level)
|
88 |
+
|
89 |
+
# Avoid adding duplicate handlers
|
90 |
+
if logger.hasHandlers():
|
91 |
+
logger.handlers.clear()
|
92 |
|
93 |
handler = logging.StreamHandler()
|
94 |
+
# Added a more detailed formatter
|
95 |
formatter = logging.Formatter(
|
96 |
+
"%(asctime)s - %(name)s - [%(levelname)s] - [%(chat_id)s] - %(funcName)s:%(lineno)d - %(message)s"
|
97 |
)
|
98 |
handler.setFormatter(formatter)
|
99 |
|
100 |
class ContextFilter(logging.Filter):
|
101 |
+
"""Injects contextual information into log records."""
|
102 |
def filter(self, record):
|
103 |
+
record.chat_id = ThreadContext.get_context().get("chat_id", "NO_CONTEXT")
|
104 |
return True
|
105 |
|
106 |
handler.addFilter(ContextFilter())
|
|
|
115 |
|
116 |
@classmethod
|
117 |
def set_context(cls, chat_id: str, message_id: str):
|
118 |
+
"""Sets the context for the current thread."""
|
119 |
cls._context.chat_id = chat_id
|
120 |
cls._context.message_id = message_id
|
121 |
|
122 |
@classmethod
|
123 |
def get_context(cls) -> Dict[str, Optional[str]]:
|
124 |
+
"""Retrieves the context for the current thread."""
|
125 |
return {
|
126 |
"chat_id": getattr(cls._context, "chat_id", None),
|
127 |
"message_id": getattr(cls._context, "message_id", None),
|
|
|
164 |
url = f"{self.base_url}/{endpoint}/{self.config.GREEN_API_TOKEN}"
|
165 |
for attempt in range(3):
|
166 |
try:
|
167 |
+
self.logger.debug(f"Sending API request to {url} with payload: {kwargs.get('json', kwargs.get('data'))}")
|
168 |
response = self.session.request(method, url, timeout=20, **kwargs)
|
169 |
response.raise_for_status()
|
170 |
return response.json()
|
|
|
246 |
try:
|
247 |
raw_response = generate_llm(system_prompt)
|
248 |
except LLMBadRequestError:
|
249 |
+
self.logger.warning(f"LLM request failed due to bad request for chat {chat_id}. Clearing history.")
|
250 |
self.conv_manager.clear_history(chat_id)
|
251 |
return SendTextIntent(action="send_text", message="Oops! Let's start fresh! π")
|
252 |
|
|
|
275 |
|
276 |
def _parse_response(self, raw_response: str) -> BaseIntent:
|
277 |
try:
|
278 |
+
# Clean the response to ensure it's valid JSON
|
279 |
+
cleaned_response = raw_response.strip().replace("`json", "").replace("`", "")
|
280 |
+
parsed = json.loads(cleaned_response)
|
281 |
for model in self.INTENT_MODELS:
|
282 |
try:
|
283 |
return model.model_validate(parsed)
|
284 |
except ValidationError:
|
285 |
continue
|
286 |
except json.JSONDecodeError:
|
287 |
+
self.logger.warning(f"Could not decode LLM response to JSON: {raw_response}")
|
288 |
|
289 |
# Fallback for non-JSON or unparsable responses
|
290 |
return SendTextIntent(action="send_text", message=raw_response)
|
|
|
307 |
@self.fastapi_app.post("/whatsapp")
|
308 |
async def webhook(request: Request):
|
309 |
if request.headers.get("Authorization") != f"Bearer {self.config.WEBHOOK_AUTH_TOKEN}":
|
310 |
+
self.logger.warning("Unauthorized webhook access attempt.")
|
311 |
raise HTTPException(403, "Unauthorized")
|
312 |
|
313 |
+
try:
|
314 |
+
payload = await request.json()
|
315 |
+
# Log the entire incoming request payload for debugging
|
316 |
+
self.logger.debug(f"Incoming webhook payload: {json.dumps(payload, indent=2)}")
|
317 |
|
318 |
+
# Process valid incoming messages in the background
|
319 |
+
if payload.get("typeWebhook") == "incomingMessageReceived":
|
320 |
+
executor.submit(self._process_incoming_message, payload)
|
321 |
+
else:
|
322 |
+
self.logger.info(f"Received non-message webhook type: {payload.get('typeWebhook')}")
|
323 |
+
except json.JSONDecodeError:
|
324 |
+
self.logger.error("Failed to decode JSON from webhook request.")
|
325 |
+
raise HTTPException(400, "Invalid JSON payload.")
|
326 |
|
327 |
return JSONResponse(content={"status": "received"})
|
328 |
|
|
|
340 |
while True:
|
341 |
task = self.task_queue.get()
|
342 |
try:
|
343 |
+
# Set context for the worker thread
|
344 |
+
ThreadContext.set_context(task["chat_id"], task["message_id"])
|
345 |
handler = getattr(self, f"_task_{task['type']}", None)
|
346 |
if handler:
|
347 |
+
self.logger.debug(f"Processing task: {task['type']} for chat {task['chat_id']}")
|
348 |
handler(task)
|
349 |
else:
|
350 |
+
self.logger.warning(f"Unknown task type received: {task['type']}")
|
351 |
except Exception as e:
|
352 |
+
self.logger.error(f"Error processing task {task.get('type', 'N/A')}: {e}", exc_info=True)
|
353 |
finally:
|
354 |
self.task_queue.task_done()
|
355 |
|
|
|
358 |
try:
|
359 |
chat_id = payload["senderData"]["chatId"]
|
360 |
message_id = payload["idMessage"]
|
361 |
+
|
362 |
+
# ** CHAT RESTRICTION LOGIC **
|
363 |
+
# Check if the sender is in the allowed list
|
364 |
+
if chat_id not in self.config.ALLOWED_CHATS:
|
365 |
+
self.logger.warning(f"Ignoring message from unauthorized chat ID: {chat_id}")
|
366 |
+
return # Stop processing immediately
|
367 |
+
|
368 |
+
# Set thread context for logging
|
369 |
ThreadContext.set_context(chat_id, message_id)
|
370 |
|
371 |
message_data = payload.get("messageData", {})
|
|
|
379 |
|
380 |
text = text.strip()
|
381 |
if not text:
|
382 |
+
self.logger.debug(f"Received empty message from {chat_id}. Ignoring.")
|
383 |
return
|
384 |
|
385 |
+
self.logger.info(f"Processing message from {chat_id}: '{text}'")
|
386 |
self.conv_manager.add_user_message(chat_id, text)
|
387 |
|
388 |
# Handle direct commands
|
|
|
392 |
# Handle natural language and replies
|
393 |
self._handle_natural_language(chat_id, message_id, text, payload)
|
394 |
|
395 |
+
except KeyError as e:
|
396 |
+
self.logger.error(f"Missing expected key in message payload: {e}. Payload: {payload}")
|
397 |
except Exception as e:
|
398 |
self.logger.error(f"Failed to process message payload: {e}", exc_info=True)
|
399 |
|
|
|
420 |
elif command == "/edit":
|
421 |
self._dispatch_edit_image(chat_id, message_id, args, payload)
|
422 |
elif command == "/joke":
|
423 |
+
self.task_queue.put({"type": "joke", "chat_id": chat_id, "message_id": message_id})
|
424 |
elif command == "/inspire":
|
425 |
+
self.task_queue.put({"type": "inspire", "chat_id": chat_id, "message_id": message_id})
|
426 |
elif command == "/weather":
|
427 |
+
self.task_queue.put({"type": "weather", "chat_id": chat_id, "message_id": message_id, "location": args})
|
428 |
else:
|
429 |
self.api_client.send_message(chat_id, "Unknown command. Type /help for options.", message_id)
|
430 |
|
|
|
442 |
# This action needs the original payload to find the replied-to image
|
443 |
self._dispatch_edit_image(chat_id, message_id, intent.prompt, payload)
|
444 |
elif hasattr(self, f"_task_{intent.action}"):
|
445 |
+
# Enqueue the task with its type and all necessary data
|
446 |
self.task_queue.put({"type": intent.action, **task_data})
|
447 |
else:
|
448 |
self.logger.warning(f"No handler found for intent action: {intent.action}")
|
|
|
450 |
|
451 |
def _dispatch_edit_image(self, chat_id, message_id, prompt, payload):
|
452 |
"""Checks for a replied-to image and dispatches the edit task."""
|
453 |
+
quoted_message = payload.get("messageData", {}).get("extendedTextMessageData", {}).get("quotedMessage")
|
454 |
if not quoted_message or quoted_message.get("typeMessage") != "imageMessage":
|
455 |
self.api_client.send_message(chat_id, "To edit an image, please reply to it with your instructions.", message_id)
|
456 |
return
|
|
|
470 |
chat_id, message_id, message = task["chat_id"], task["message_id"], task["message"]
|
471 |
self.api_client.send_message(chat_id, message, message_id)
|
472 |
self.conv_manager.add_bot_message(chat_id, message)
|
473 |
+
# Asynchronously generate a voice reply for the sent text
|
474 |
self.task_queue.put({"type": "voice_reply", "chat_id": chat_id, "message_id": message_id, "text": message})
|
475 |
|
476 |
def _task_generate_image(self, task: Dict[str, Any]):
|
|
|
484 |
self.api_client.send_file(chat_id, path, caption, mid)
|
485 |
os.remove(path)
|
486 |
except Exception as e:
|
487 |
+
self.logger.error(f"Image generation {i+1} failed: {e}", exc_info=True)
|
488 |
self.api_client.send_message(chat_id, f"π’ Failed to generate image {i+1}.", mid)
|
489 |
|
490 |
def _task_edit_image(self, task: Dict[str, Any]):
|
|
|
497 |
if not image_data:
|
498 |
raise ValueError("Failed to download image.")
|
499 |
|
500 |
+
os.makedirs(self.config.TEMP_DIR, exist_ok=True)
|
501 |
input_path = os.path.join(self.config.TEMP_DIR, f"input_{mid}.jpg")
|
502 |
output_path = os.path.join(self.config.TEMP_DIR, f"output_{mid}.jpg")
|
503 |
|
|
|
513 |
raise ValueError("Edited image file not found.")
|
514 |
|
515 |
except Exception as e:
|
516 |
+
self.logger.error(f"Image editing task failed: {e}", exc_info=True)
|
517 |
self.api_client.send_message(chat_id, "π’ Sorry, I failed to edit the image.", mid)
|
518 |
finally:
|
519 |
for path in [input_path, output_path]:
|
|
|
530 |
self.api_client.send_file(task["chat_id"], path, quoted_message_id=task["message_id"])
|
531 |
os.remove(path)
|
532 |
except Exception as e:
|
533 |
+
self.logger.warning(f"Voice reply generation failed: {e}", exc_info=True)
|
534 |
|
535 |
def _task_joke(self, task: Dict[str, Any]):
|
536 |
try:
|
537 |
j = requests.get("https://official-joke-api.appspot.com/random_joke", timeout=5).json()
|
538 |
joke = f"{j['setup']}\n\n{j['punchline']}"
|
539 |
except Exception:
|
540 |
+
self.logger.warning("Joke API failed, falling back to LLM.")
|
541 |
joke = generate_llm("Tell me a short, clean joke.")
|
542 |
self._task_send_text({"type": "send_text", **task, "message": f"π {joke}"})
|
543 |
|
|
|
546 |
self._task_send_text({"type": "send_text", **task, "message": f"β¨ {quote}"})
|
547 |
|
548 |
def _task_weather(self, task: Dict[str, Any]):
|
549 |
+
location = task.get("location")
|
550 |
+
if not location:
|
551 |
+
self.api_client.send_message(task["chat_id"], "Please provide a location for the weather.", task["message_id"])
|
552 |
+
return
|
553 |
try:
|
554 |
raw = requests.get(f"http://wttr.in/{location.replace(' ', '+')}?format=4", timeout=10).text
|
555 |
report = generate_llm(f"Create a friendly weather report in Celsius from this data:\n\n{raw}")
|
556 |
self._task_send_text({"type": "send_text", **task, "message": f"π€οΈ Weather for {location}:\n{report}"})
|
557 |
except Exception as e:
|
558 |
+
self.logger.error(f"Weather task failed: {e}", exc_info=True)
|
559 |
self.api_client.send_message(task["chat_id"], "Sorry, I couldn't get the weather.", task["message_id"])
|
560 |
|
561 |
def run(self):
|
|
|
563 |
self.logger.info("Starting Eve WhatsApp Bot...")
|
564 |
for d in [self.config.IMAGE_DIR, self.config.AUDIO_DIR, self.config.TEMP_DIR]:
|
565 |
os.makedirs(d, exist_ok=True)
|
566 |
+
self.logger.debug(f"Ensured directory exists: {d}")
|
567 |
|
568 |
+
# Send a startup message to one of the allowed chats to confirm it's online
|
569 |
self.api_client.send_message(
|
570 | |
571 |
"π Eve is online and ready to help! Type /help to see commands."
|
572 |
)
|
573 |
|
574 |
+
uvicorn.run(self.fastapi_app, host="0.0.0.0", port=7860, log_config=None)
|
575 |
|
576 |
|
577 |
if __name__ == "__main__":
|
578 |
try:
|
579 |
+
bot_config = BotConfig()
|
580 |
+
executor = ThreadPoolExecutor(max_workers=bot_config.WORKER_THREADS * 2)
|
581 |
+
bot = WhatsAppBot(bot_config)
|
582 |
bot.run()
|
583 |
except ValueError as e:
|
584 |
# Catch config validation errors
|
585 |
+
logging.basicConfig()
|
586 |
+
logging.getLogger().critical(f"β CONFIGURATION ERROR: {e}")
|
587 |
except KeyboardInterrupt:
|
588 |
print("\nπ Bot stopped by user.")
|
589 |
except Exception as e:
|
590 |
+
logging.basicConfig()
|
591 |
+
logging.getLogger().critical(f"β A fatal error occurred: {e}", exc_info=True)
|
polLLM.py
CHANGED
@@ -11,7 +11,7 @@ load_dotenv()
|
|
11 |
_config = read_config()["llm"]
|
12 |
|
13 |
# --- Logging setup ---
|
14 |
-
LOG_LEVEL = os.getenv("LOG_LEVEL", "
|
15 |
logger = logging.getLogger("polLLM")
|
16 |
logger.setLevel(LOG_LEVEL)
|
17 |
handler = logging.StreamHandler()
|
|
|
11 |
_config = read_config()["llm"]
|
12 |
|
13 |
# --- Logging setup ---
|
14 |
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
|
15 |
logger = logging.getLogger("polLLM")
|
16 |
logger.setLevel(LOG_LEVEL)
|
17 |
handler = logging.StreamHandler()
|