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
+
+
+
+
+
+
+
+{% 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" }
+ }]
+ }
+ }]
+ }]
+}
+