feat: add appointments, campaigns, admin, storage, tests and various updates

- Add new routers: admin, appointments, campaigns
- Add storage service and logging config
- Add migrations directory and test suite with pytest config
- Add supabase_migration_features.sql
- Update models, dependencies, config, and existing routers
- Remove whatsapp_service (deleted)
- Update pyproject.toml and uv.lock dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
belviskhoremk
2026-04-03 09:11:58 +00:00
parent 9dccc83293
commit 92d4c2fc5e
51 changed files with 7076 additions and 515 deletions

View File

@@ -9,6 +9,7 @@ from app.database import get_supabase
from app.dependencies import get_current_user
from app.config import PLAN_LIMITS
from typing import List, Optional, Dict
from collections import defaultdict
from pydantic import BaseModel
from datetime import datetime, timedelta
import logging
@@ -127,14 +128,7 @@ async def get_analytics_overview(user=Depends(get_current_user)):
conversations_used=0,
)
# Gather per-chatbot analytics
chatbot_analytics = []
total_convos = 0
total_msgs = 0
total_sessions = 0
month_convos = 0
all_ratings = []
# ── Batch queries (fixes N+1) ────────────────────────────────────────────────
now = datetime.utcnow()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
week_start = now - timedelta(days=now.weekday())
@@ -142,14 +136,60 @@ async def get_analytics_overview(user=Depends(get_current_user)):
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
thirty_days_ago = now - timedelta(days=30)
# Batch query 1: ALL conversations for all chatbots (single query)
all_convos_resp = supabase.table("conversations") \
.select("id, chatbot_id, session_id, language, created_at") \
.in_("chatbot_id", chatbot_ids) \
.execute()
all_convos = all_convos_resp.data or []
all_conv_ids = [c["id"] for c in all_convos]
# Batch query 2: ALL messages for all conversations (single query)
all_msgs: List[Dict] = []
if all_conv_ids:
# Split into chunks of 500 to avoid URL length limits
for i in range(0, len(all_conv_ids), 500):
chunk = all_conv_ids[i:i + 500]
msgs_resp = supabase.table("messages") \
.select("id, conversation_id, role, content, created_at") \
.in_("conversation_id", chunk) \
.execute()
all_msgs.extend(msgs_resp.data or [])
# Batch query 3: ALL feedback for all chatbots (single query)
all_feedback: List[Dict] = []
if chatbot_ids:
fb_resp = supabase.table("message_feedback") \
.select("chatbot_id, feedback") \
.in_("chatbot_id", chatbot_ids) \
.execute()
all_feedback = fb_resp.data or []
# Index data by chatbot_id for O(1) lookups
convos_by_chatbot: Dict[str, List[Dict]] = defaultdict(list)
for c in all_convos:
convos_by_chatbot[c["chatbot_id"]].append(c)
msgs_by_conv: Dict[str, List[Dict]] = defaultdict(list)
for m in all_msgs:
msgs_by_conv[m["conversation_id"]].append(m)
fb_by_chatbot: Dict[str, List[Dict]] = defaultdict(list)
for f in all_feedback:
fb_by_chatbot[f["chatbot_id"]].append(f)
# ── Aggregate per chatbot ────────────────────────────────────────────────────
chatbot_analytics = []
total_convos = 0
total_msgs = 0
total_sessions = 0
month_convos = 0
all_ratings = []
for chatbot in chatbot_list:
cid = chatbot["id"]
# Total conversations
convos = supabase.table("conversations").select("id, session_id, language, created_at", count="exact") \
.eq("chatbot_id", cid).execute()
conv_count = convos.count or 0
conv_data = convos.data or []
conv_data = convos_by_chatbot[cid]
conv_count = len(conv_data)
total_convos += conv_count
# Unique sessions
@@ -157,34 +197,35 @@ async def get_analytics_overview(user=Depends(get_current_user)):
unique_sess = len(sessions)
total_sessions += unique_sess
# Total messages
msgs = supabase.table("messages").select("id", count="exact") \
.in_("conversation_id", [c["id"] for c in conv_data] if conv_data else [""]).execute()
msg_count = msgs.count or 0
# Messages for this chatbot
chatbot_msgs = []
for c in conv_data:
chatbot_msgs.extend(msgs_by_conv[c["id"]])
msg_count = len(chatbot_msgs)
total_msgs += msg_count
# Time-based conversation counts
today_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"][:10] == today_start.strftime("%Y-%m-%d"))
today_str = today_start.strftime("%Y-%m-%d")
today_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"][:10] == today_str)
week_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"] >= week_start.isoformat())
month_count = sum(1 for c in conv_data if c.get("created_at") and c["created_at"] >= month_start.isoformat())
month_convos += month_count
# Daily conversations (last 30 days)
daily = {}
daily: Dict[str, int] = {}
for c in conv_data:
if c.get("created_at") and c["created_at"] >= thirty_days_ago.isoformat():
day = c["created_at"][:10]
daily[day] = daily.get(day, 0) + 1
daily_list = [DailyConversations(date=d, count=n) for d, n in sorted(daily.items())]
# Languages used
# Languages
lang_counts: Dict[str, int] = {}
for c in conv_data:
lang = c.get("language", "en")
lang_counts[lang] = lang_counts.get(lang, 0) + 1
# Peak hour (approximate from created_at)
# Peak hour
hour_counts: Dict[int, int] = {}
for c in conv_data:
if c.get("created_at") and len(c["created_at"]) > 13:
@@ -195,35 +236,25 @@ async def get_analytics_overview(user=Depends(get_current_user)):
pass
peak = max(hour_counts, key=hour_counts.get) if hour_counts else None
# Top queries (from user messages, get first message per conversation)
top_queries: List[TopQuery] = []
if conv_data:
conv_ids = [c["id"] for c in conv_data[:100]] # limit to recent 100
user_msgs = supabase.table("messages").select("content") \
.in_("conversation_id", conv_ids) \
.eq("role", "user") \
.limit(200).execute()
query_counts: Dict[str, int] = {}
for m in (user_msgs.data or []):
# Top queries from user messages
query_counts: Dict[str, int] = {}
for m in chatbot_msgs:
if m.get("role") == "user":
content = (m.get("content") or "")[:100].strip()
if content:
query_counts[content] = query_counts.get(content, 0) + 1
top_sorted = sorted(query_counts.items(), key=lambda x: -x[1])[:5]
top_queries = [TopQuery(query=q, count=n) for q, n in top_sorted]
top_queries = [TopQuery(query=q, count=n) for q, n in sorted(query_counts.items(), key=lambda x: -x[1])[:5]]
# Rating
rating = chatbot.get("average_rating")
if rating:
all_ratings.append(rating)
# Feedback counts
fb_result = supabase.table("message_feedback").select("feedback", count="exact") \
.eq("chatbot_id", cid).execute()
total_fb = fb_result.count or 0
fb_pos = sum(1 for f in (fb_result.data or []) if f.get("feedback") == "positive")
fb_neg = total_fb - fb_pos
# Feedback
chatbot_fb = fb_by_chatbot[cid]
fb_pos = sum(1 for f in chatbot_fb if f.get("feedback") == "positive")
fb_neg = len(chatbot_fb) - fb_pos
# Average messages per conversation
avg_msgs = round(msg_count / conv_count, 1) if conv_count > 0 else 0.0
chatbot_analytics.append(ChatbotAnalyticsResponse(
@@ -234,7 +265,7 @@ async def get_analytics_overview(user=Depends(get_current_user)):
total_messages=msg_count,
average_messages_per_conversation=avg_msgs,
average_rating=rating,
total_ratings=total_fb,
total_ratings=len(chatbot_fb),
conversations_today=today_count,
conversations_this_week=week_count,
conversations_this_month=month_count,