added analytics

This commit is contained in:
belviskhoremk
2026-02-23 17:24:41 +00:00
parent 07c4c55072
commit 2ed998058e
5 changed files with 543 additions and 105 deletions

364
app/routers/analytics.py Normal file
View File

@@ -0,0 +1,364 @@
"""
Analytics router - provides chatbot performance data for Starter+ users.
Available to: Starter, Pro, Enterprise plans only.
No LLM cost data is exposed to users.
"""
from fastapi import APIRouter, HTTPException, Depends
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 pydantic import BaseModel
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/analytics", tags=["Analytics"])
# ─── Response Models ───────────────────────────────────────────────────────────
class DailyConversations(BaseModel):
date: str
count: int
class TopQuery(BaseModel):
query: str
count: int
class ChatbotAnalyticsResponse(BaseModel):
chatbot_id: str
chatbot_name: str
total_conversations: int
unique_sessions: int
total_messages: int
average_messages_per_conversation: float
average_rating: Optional[float]
total_ratings: int
conversations_today: int
conversations_this_week: int
conversations_this_month: int
daily_conversations: List[DailyConversations]
top_queries: List[TopQuery]
languages_used: Dict[str, int]
peak_hour: Optional[int] # 0-23
class OverviewAnalyticsResponse(BaseModel):
total_chatbots: int
published_chatbots: int
total_conversations: int
total_messages: int
unique_sessions: int
conversations_this_month: int
average_rating: Optional[float]
chatbots: List[ChatbotAnalyticsResponse]
plan: str
conversations_limit: int
conversations_used: int
# ─── Helpers ───────────────────────────────────────────────────────────────────
def _get_user_plan(user_id: str) -> str:
supabase = get_supabase()
result = supabase.table("subscriptions") \
.select("plan") \
.eq("user_id", user_id) \
.eq("status", "active") \
.execute()
return result.data[0]["plan"] if result.data else "free"
def _check_analytics_access(plan: str):
"""Ensure user has analytics access (Starter+)."""
plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"])
if not plan_config.get("analytics", False):
raise HTTPException(
status_code=402,
detail="Analytics is available on Starter and Pro plans. Upgrade to access your chatbot analytics."
)
# ─── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("/overview", response_model=OverviewAnalyticsResponse)
async def get_analytics_overview(user=Depends(get_current_user)):
"""
Get analytics overview across all chatbots for the current user.
Requires Starter+ plan.
"""
plan = _get_user_plan(user.id)
_check_analytics_access(plan)
supabase = get_supabase()
# Get user's company
company = supabase.table("companies").select("id").eq("owner_id", user.id).execute()
if not company.data:
raise HTTPException(status_code=404, detail="Company not found")
company_id = company.data[0]["id"]
# Get all chatbots
chatbots = supabase.table("chatbots").select("*").eq("company_id", company_id).execute()
chatbot_list = chatbots.data or []
chatbot_ids = [c["id"] for c in chatbot_list]
if not chatbot_ids:
plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"])
return OverviewAnalyticsResponse(
total_chatbots=0,
published_chatbots=0,
total_conversations=0,
total_messages=0,
unique_sessions=0,
conversations_this_month=0,
average_rating=None,
chatbots=[],
plan=plan,
conversations_limit=plan_config.get("conversations_limit", 0),
conversations_used=0,
)
# Gather per-chatbot analytics
chatbot_analytics = []
total_convos = 0
total_msgs = 0
total_sessions = 0
month_convos = 0
all_ratings = []
now = datetime.utcnow()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
week_start = now - timedelta(days=now.weekday())
week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
thirty_days_ago = now - timedelta(days=30)
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 []
total_convos += conv_count
# Unique sessions
sessions = set(c.get("session_id") for c in conv_data if c.get("session_id"))
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
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"))
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 = {}
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
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)
hour_counts: Dict[int, int] = {}
for c in conv_data:
if c.get("created_at") and len(c["created_at"]) > 13:
try:
hour = int(c["created_at"][11:13])
hour_counts[hour] = hour_counts.get(hour, 0) + 1
except (ValueError, IndexError):
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 []):
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]
# Rating
rating = chatbot.get("average_rating")
if rating:
all_ratings.append(rating)
# Average messages per conversation
avg_msgs = round(msg_count / conv_count, 1) if conv_count > 0 else 0.0
chatbot_analytics.append(ChatbotAnalyticsResponse(
chatbot_id=cid,
chatbot_name=chatbot.get("name", "Untitled"),
total_conversations=conv_count,
unique_sessions=unique_sess,
total_messages=msg_count,
average_messages_per_conversation=avg_msgs,
average_rating=rating,
total_ratings=0, # would need a ratings table for precise count
conversations_today=today_count,
conversations_this_week=week_count,
conversations_this_month=month_count,
daily_conversations=daily_list,
top_queries=top_queries,
languages_used=lang_counts,
peak_hour=peak,
))
# Overall average rating
avg_rating = round(sum(all_ratings) / len(all_ratings), 1) if all_ratings else None
plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"])
return OverviewAnalyticsResponse(
total_chatbots=len(chatbot_list),
published_chatbots=sum(1 for c in chatbot_list if c.get("is_published")),
total_conversations=total_convos,
total_messages=total_msgs,
unique_sessions=total_sessions,
conversations_this_month=month_convos,
average_rating=avg_rating,
chatbots=chatbot_analytics,
plan=plan,
conversations_limit=plan_config.get("conversations_limit", 0),
conversations_used=month_convos,
)
@router.get("/chatbot/{chatbot_id}", response_model=ChatbotAnalyticsResponse)
async def get_chatbot_analytics(chatbot_id: str, user=Depends(get_current_user)):
"""
Get detailed analytics for a specific chatbot.
Requires Starter+ plan and ownership of the chatbot.
"""
plan = _get_user_plan(user.id)
_check_analytics_access(plan)
supabase = get_supabase()
# Verify ownership
company = supabase.table("companies").select("id").eq("owner_id", user.id).execute()
if not company.data:
raise HTTPException(status_code=404, detail="Company not found")
chatbot = supabase.table("chatbots").select("*") \
.eq("id", chatbot_id) \
.eq("company_id", company.data[0]["id"]).execute()
if not chatbot.data:
raise HTTPException(status_code=404, detail="Chatbot not found")
cb = chatbot.data[0]
now = datetime.utcnow()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
week_start = now - timedelta(days=now.weekday())
week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
thirty_days_ago = now - timedelta(days=30)
# Conversations
convos = supabase.table("conversations").select("id, session_id, language, created_at", count="exact") \
.eq("chatbot_id", chatbot_id).execute()
conv_count = convos.count or 0
conv_data = convos.data or []
sessions = set(c.get("session_id") for c in conv_data if c.get("session_id"))
# Messages
conv_ids = [c["id"] for c in conv_data] if conv_data else [""]
msgs = supabase.table("messages").select("id", count="exact") \
.in_("conversation_id", conv_ids).execute()
msg_count = msgs.count or 0
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"))
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())
# Daily
daily = {}
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
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
hour_counts: Dict[int, int] = {}
for c in conv_data:
if c.get("created_at") and len(c["created_at"]) > 13:
try:
hour = int(c["created_at"][11:13])
hour_counts[hour] = hour_counts.get(hour, 0) + 1
except (ValueError, IndexError):
pass
peak = max(hour_counts, key=hour_counts.get) if hour_counts else None
# Top queries
top_queries: List[TopQuery] = []
if conv_data:
recent_ids = [c["id"] for c in conv_data[:100]]
user_msgs = supabase.table("messages").select("content") \
.in_("conversation_id", recent_ids) \
.eq("role", "user") \
.limit(200).execute()
query_counts: Dict[str, int] = {}
for m in (user_msgs.data or []):
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])[:10]
top_queries = [TopQuery(query=q, count=n) for q, n in top_sorted]
avg_msgs = round(msg_count / conv_count, 1) if conv_count > 0 else 0.0
return ChatbotAnalyticsResponse(
chatbot_id=chatbot_id,
chatbot_name=cb.get("name", "Untitled"),
total_conversations=conv_count,
unique_sessions=len(sessions),
total_messages=msg_count,
average_messages_per_conversation=avg_msgs,
average_rating=cb.get("average_rating"),
total_ratings=0,
conversations_today=today_count,
conversations_this_week=week_count,
conversations_this_month=month_count,
daily_conversations=daily_list,
top_queries=top_queries,
languages_used=lang_counts,
peak_hour=peak,
)

View File

@@ -129,32 +129,14 @@ async def get_chat_history(
]
# ── Analytics endpoint ────────────────────────────────────────────────────────
@router.get("/analytics/{chatbot_id}")
async def get_analytics(chatbot_id: str, user=Depends(get_current_user)):
supabase = get_supabase()
# Verify ownership
company = supabase.table("companies").select("id").eq("owner_id", user.id).execute()
if not company.data:
raise HTTPException(status_code=404, detail="Company not found")
chatbot = supabase.table("chatbots").select("id").eq("id", chatbot_id).eq("company_id",
company.data[0]["id"]).execute()
if not chatbot.data:
raise HTTPException(status_code=404, detail="Chatbot not found")
total_convs = supabase.table("conversations").select("id", count="exact").eq("chatbot_id", chatbot_id).execute()
total_msgs = supabase.table("messages").select("id", count="exact").execute()
return {
"chatbot_id": chatbot_id,
"total_conversations": total_convs.count or 0,
"total_messages": total_msgs.count or 0,
"average_rating": 0.0,
"conversations_last_30_days": total_convs.count or 0,
}
# ── OLD analytics endpoint REMOVED ───────────────────────────────────────────
# The /analytics/{chatbot_id} endpoint that was here has been replaced by
# the dedicated analytics router (app/routers/analytics.py) which provides:
# GET /api/v1/analytics/overview — All chatbots overview
# GET /api/v1/analytics/chatbot/{id} — Single chatbot detail
# The old endpoint conflicted with the new router because FastAPI matched
# "overview" as a chatbot_id UUID, causing a 500 error.
# ─────────────────────────────────────────────────────────────────────────────
# ── Helpers ───────────────────────────────────────────────────────────────────
@@ -188,12 +170,8 @@ def _get_or_create_conversation(
def _get_conversation_history(conversation_id: str, supabase) -> List[dict]:
"""
FIX: Changed from desc=True to desc=False (ascending/chronological order).
The conversation history MUST be in chronological order (oldest first)
Returns conversation history in chronological order (oldest first)
for the LLM to correctly understand the conversation flow.
Previously, messages were returned newest-first, which reversed the
conversation and confused the model.
"""
messages = supabase.table("messages").select("role, content") \
.eq("conversation_id", conversation_id) \

View File

@@ -8,14 +8,62 @@ import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/marketplace", tags=["Marketplace"])
# ═══════════════════════════════════════════════════════════════════════════════
# CATEGORIES & INDUSTRIES — Expanded to support all business types:
# Individuals, small businesses (restaurants, barbershops, malls, phone shops),
# and large enterprises.
# ═══════════════════════════════════════════════════════════════════════════════
CATEGORIES = [
"Customer Support", "Sales", "FAQ", "E-commerce",
"Healthcare", "Finance", "Education", "HR", "Legal", "Other"
# What the chatbot does
"Customer Support",
"Sales Assistant",
"FAQ & Knowledge Base",
"Appointment Booking",
"Order & Delivery Tracking",
"Product Recommendations",
"Lead Generation",
"Onboarding & Training",
"Feedback & Surveys",
"Personal Assistant",
"Consultation",
"Other",
]
INDUSTRIES = [
"Technology", "E-commerce", "Healthcare", "Finance",
"Education", "Legal", "Real Estate", "Hospitality", "Retail", "Other"
# Small businesses / Local services
"Restaurant & Food",
"Beauty & Barbershop",
"Retail & Shopping",
"Phone & Electronics",
"Automotive & Repair",
"Fitness & Wellness",
"Cleaning & Home Services",
"Photography & Events",
# Professional services
"Healthcare & Medical",
"Legal & Law",
"Finance & Insurance",
"Real Estate",
"Accounting & Tax",
# Tech & Digital
"Technology & SaaS",
"E-commerce",
"Agency & Marketing",
# Education & Non-profit
"Education & Training",
"Non-profit & NGO",
# Large scale
"Hospitality & Hotels",
"Travel & Tourism",
"Manufacturing",
"Logistics & Transport",
"Agriculture",
"Government",
# Personal
"Personal Brand",
"Freelancer & Consultant",
"Other",
]