From f2180483f02a58e33c03bcacf439bfc8538045fa Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Mon, 19 May 2025 23:04:20 -0600 Subject: [PATCH] Adding DB to Whatsaap AI --- pxy_dashboard/apps/views.py | 13 +++- pxy_dashboard/middleware.py | 11 +++ .../pxy_dashboard/apps/apps-whatsapp-bot.html | 54 +++++++++++++- .../migrations/0004_conversation_message.py | 34 +++++++++ pxy_whatsapp/models.py | 32 +++++++++ pxy_whatsapp/views.py | 72 +++++++++++++++++-- tests/test_payload.json | 15 ++++ 7 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 pxy_whatsapp/migrations/0004_conversation_message.py create mode 100644 tests/test_payload.json diff --git a/pxy_dashboard/apps/views.py b/pxy_dashboard/apps/views.py index 2928088..7c50d22 100644 --- a/pxy_dashboard/apps/views.py +++ b/pxy_dashboard/apps/views.py @@ -31,7 +31,7 @@ apps_dispatch_plan = AppsView.as_view(template_name="pxy_dashboard/apps/apps-dis # Operation – Physical & Social Digital Twin apps_urban_digital_twin = AppsView.as_view(template_name="pxy_dashboard/apps/apps-urban-digital-twin.html") -apps_whatsapp_bot = AppsView.as_view(template_name="pxy_dashboard/apps/apps-whatsapp-bot.html") +#apps_whatsapp_bot = AppsView.as_view(template_name="pxy_dashboard/apps/apps-whatsapp-bot.html") apps_telegram_bot = AppsView.as_view(template_name="pxy_dashboard/apps/apps-telegram-bot.html") apps_facebook_pages_bot = AppsView.as_view(template_name="pxy_dashboard/apps/apps-facebook-pages-bot.html") apps_feedback_loop = AppsView.as_view(template_name="pxy_dashboard/apps/apps-feedback-loop.html") @@ -268,3 +268,14 @@ def dispatch_plan_view(request): "selected_subdivision": selected_subdivision, "selected_route": selected_route, }) + + +from django.contrib.auth.decorators import login_required +from django.shortcuts import render +import requests + +@login_required +def apps_whatsapp_bot(request): + stats = request.user.has_perm("pxy_whatsapp.view_whatsappstats") and \ + requests.get(request.build_absolute_uri("/whatsapp/stats/"), cookies=request.COOKIES).json() + return render(request, "pxy_dashboard/apps/apps-whatsapp-bot.html", {"stats": stats}) diff --git a/pxy_dashboard/middleware.py b/pxy_dashboard/middleware.py index 7336d3d..733fcde 100644 --- a/pxy_dashboard/middleware.py +++ b/pxy_dashboard/middleware.py @@ -22,6 +22,17 @@ EXEMPT_URLS += [re.compile(expr) for expr in [ r"^media/", ]] +# ————— aquí añadimos los webhooks de WhatsApp ————— +# Como path_info.lstrip("/") será "whatsapp/webhook/" y "whatsapp/webhook/verify/" +EXEMPT_URLS += [ + "pxy_whatsapp/webhook/", + "pxy_whatsapp/webhook/verify/", +] +EXEMPT_URLS += [ + re.compile(r"^pxy_whatsapp/webhook/?$"), + re.compile(r"^pxy_whatsapp/webhook/verify/?$"), +] + class LoginRequiredMiddleware(MiddlewareMixin): def process_request(self, request): diff --git a/pxy_dashboard/templates/pxy_dashboard/apps/apps-whatsapp-bot.html b/pxy_dashboard/templates/pxy_dashboard/apps/apps-whatsapp-bot.html index 0c0c871..b61d09d 100644 --- a/pxy_dashboard/templates/pxy_dashboard/apps/apps-whatsapp-bot.html +++ b/pxy_dashboard/templates/pxy_dashboard/apps/apps-whatsapp-bot.html @@ -1,5 +1,55 @@ {% extends "pxy_dashboard/partials/base.html" %} {% block content %} -

Whatsapp Bot

-

This is a placeholder page for apps-whatsapp-bot.html

+{% include "pxy_dashboard/partials/dashboard/kpi_row.html" %} + +
+
+
+
+

{{ stats.total_conversations }}

+

Conversaciones totales

+
+
+
+
+
+
+

{{ stats.messages_in }}

+

Mensajes recibidos (24h)

+
+
+
+
+
+
+

{{ stats.messages_out }}

+

Mensajes enviados (24h)

+
+
+
+
+
+
+

{{ stats.avg_response_time|floatformat:0 }} ms

+

Tiempo de respuesta promedio

+
+
+
+
+ +
+
Actividad por hora
+
+
+
+
+ +{% block extra_js %} + + +{% endblock %} + {% endblock %} diff --git a/pxy_whatsapp/migrations/0004_conversation_message.py b/pxy_whatsapp/migrations/0004_conversation_message.py new file mode 100644 index 0000000..dc71761 --- /dev/null +++ b/pxy_whatsapp/migrations/0004_conversation_message.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.3 on 2025-05-20 03:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pxy_whatsapp', '0003_whatsappbot_assistant'), + ] + + operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_number', models.CharField(max_length=32)), + ('started_at', models.DateTimeField(auto_now_add=True)), + ('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='pxy_whatsapp.whatsappbot')), + ], + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('direction', models.CharField(choices=[('in', 'In'), ('out', 'Out')], max_length=4)), + ('content', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('response_time_ms', models.IntegerField(blank=True, null=True)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='pxy_whatsapp.conversation')), + ], + ), + ] diff --git a/pxy_whatsapp/models.py b/pxy_whatsapp/models.py index bc2e98f..ee6e76e 100644 --- a/pxy_whatsapp/models.py +++ b/pxy_whatsapp/models.py @@ -16,3 +16,35 @@ class WhatsAppBot(models.Model): def __str__(self): return self.name + + +class Conversation(models.Model): + bot = models.ForeignKey( + 'WhatsAppBot', + on_delete=models.CASCADE, + related_name='conversations' + ) + user_number = models.CharField(max_length=32) + started_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.user_number} @ {self.started_at:%Y-%m-%d %H:%M}" + +class Message(models.Model): + DIRECTION_CHOICES = [ + ('in', 'In'), + ('out', 'Out'), + ] + conversation = models.ForeignKey( + Conversation, + on_delete=models.CASCADE, + related_name='messages' + ) + direction = models.CharField(max_length=4, choices=DIRECTION_CHOICES) + content = models.TextField() + timestamp = models.DateTimeField(auto_now_add=True) + response_time_ms = models.IntegerField(null=True, blank=True) + + def __str__(self): + return f"[{self.direction}] {self.content[:30]}…" + diff --git a/pxy_whatsapp/views.py b/pxy_whatsapp/views.py index 34966bc..3121cf2 100644 --- a/pxy_whatsapp/views.py +++ b/pxy_whatsapp/views.py @@ -8,6 +8,9 @@ from django.views.decorators.http import require_http_methods from django.shortcuts import get_object_or_404 from pxy_openai.assistants import OpenAIAssistant from .models import WhatsAppBot +from .models import WhatsAppBot, Conversation, Message +from django.utils import timezone + logger = logging.getLogger(__name__) @@ -76,24 +79,59 @@ def webhook(request): value, message = parse_webhook_payload(payload) if message.get("type") == "text": - user_message = message["text"]["body"] + user_message = message["text"]["body"] phone_number_id = value.get("metadata", {}).get("phone_number_id") - sender_number = message["from"] + sender_number = message["from"] logger.info(f"Received phone_number_id from webhook payload: {phone_number_id}") - # Fetch the appropriate bot configuration - bot = get_object_or_404(WhatsAppBot, phone_number_id=phone_number_id, is_active=True) - # Initialize the assistant and get a response + # 1) Fetch the active bot + bot = get_object_or_404( + WhatsAppBot, + phone_number_id=phone_number_id, + is_active=True + ) + + # 2) Get or create Conversation + conv, _ = Conversation.objects.get_or_create( + bot=bot, + user_number=sender_number, + defaults={'started_at': timezone.now()} + ) + + # 3) Save inbound message + Message.objects.create( + conversation=conv, + direction="in", + content=user_message + ) + + # 4) Generate assistant response and measure time assistant = OpenAIAssistant(name=bot.assistant.name) + start = timezone.now() try: bot_response = assistant.handle_message(user_message) except Exception as e: bot_response = f"Assistant error: {e}" logger.error(bot_response) + end = timezone.now() - # Send the response back to the user - send_whatsapp_message(bot.phone_number_id, sender_number, bot_response, bot.graph_api_token) + # 5) Send the response back to the user + send_whatsapp_message( + phone_number_id, + sender_number, + bot_response, + bot.graph_api_token + ) + + # 6) Save outbound message with response time + resp_ms = int((end - start).total_seconds() * 1000) + Message.objects.create( + conversation=conv, + direction="out", + content=bot_response, + response_time_ms=resp_ms + ) except Exception as e: logger.error(f"Error processing webhook: {e}") @@ -103,6 +141,7 @@ def webhook(request): return HttpResponse("Method Not Allowed", status=405) + # Webhook Verification Endpoint @require_http_methods(["GET"]) def webhook_verification(request): @@ -126,3 +165,22 @@ def root(request): A root endpoint for basic connectivity testing. """ return HttpResponse("
Nothing to see here.\nCheckout README.md to start.
", content_type="text/html") + +from django.contrib.auth.decorators import login_required +from django.db.models import Avg +from .models import Conversation, Message + +@login_required +def whatsapp_stats(request): + from django.utils import timezone + since = timezone.now() - timezone.timedelta(days=1) + total_convos = Conversation.objects.count() + msgs_in = Message.objects.filter(direction="in", timestamp__gte=since).count() + msgs_out = Message.objects.filter(direction="out", timestamp__gte=since).count() + avg_rt = Message.objects.filter(direction="out", response_time_ms__isnull=False).aggregate(Avg("response_time_ms")) + return JsonResponse({ + "total_conversations": total_convos, + "messages_in": msgs_in, + "messages_out": msgs_out, + "avg_response_time": avg_rt["response_time_ms__avg"] or 0, + }) diff --git a/tests/test_payload.json b/tests/test_payload.json new file mode 100644 index 0000000..c32d993 --- /dev/null +++ b/tests/test_payload.json @@ -0,0 +1,15 @@ +{ + "entry": [{ + "changes": [{ + "value": { + "metadata": { "phone_number_id": "1234567890" }, + "messages": [{ + "from": "5491123456789", + "type": "text", + "text": { "body": "Prueba desde curl" } + }] + } + }] + }] +} +