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()
|