mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
added analytics
This commit is contained in:
182
app/config.py
182
app/config.py
@@ -66,69 +66,72 @@ settings = Settings()
|
||||
|
||||
MODEL_CATALOG = {
|
||||
# ── Free tier (Fireworks - lightweight) ────────────────────────────────────
|
||||
"accounts/fireworks/models/kimi-k2-instruct-0905": {
|
||||
"name": "Kimi K2",
|
||||
"accounts/fireworks/models/llama-v3p3-70b-instruct": {
|
||||
"name": "Llama 3.3 70B",
|
||||
"provider": "Fireworks AI",
|
||||
"badge": "Free",
|
||||
"description": "Free model for building and testing chatbots",
|
||||
},
|
||||
|
||||
# ── Starter tier (Fireworks - powerful) ────────────────────────────────────
|
||||
"accounts/fireworks/models/llama-v3p1-70b-instruct": {
|
||||
"name": "Llama 3.1 70B",
|
||||
# ── Starter tier (Fireworks - powerful serverless models) ──────────────────
|
||||
"accounts/fireworks/models/qwen3-235b-a22b": {
|
||||
"name": "Qwen3 235B",
|
||||
"provider": "Fireworks AI",
|
||||
"badge": "Fast",
|
||||
"description": "Fast open-source model, great for most tasks",
|
||||
"badge": "Powerful",
|
||||
"description": "High-capability open model with great reasoning",
|
||||
},
|
||||
"accounts/fireworks/models/mixtral-8x7b-instruct": {
|
||||
"name": "Mixtral 8x7B",
|
||||
"accounts/fireworks/models/deepseek-v3p1": {
|
||||
"name": "DeepSeek V3.1",
|
||||
"provider": "Fireworks AI",
|
||||
"badge": "Balanced",
|
||||
"description": "Balanced speed and quality",
|
||||
"badge": "Smart",
|
||||
"description": "Cost-effective and highly capable model",
|
||||
},
|
||||
"accounts/fireworks/models/qwen2p5-72b-instruct": {
|
||||
"name": "Qwen 2.5 72B",
|
||||
"accounts/fireworks/models/kimi-k2-instruct-0905": {
|
||||
"name": "Kimi K2",
|
||||
"provider": "Fireworks AI",
|
||||
"badge": "Multilingual",
|
||||
"description": "Excellent multilingual capabilities",
|
||||
"description": "Strong multilingual and coding capabilities",
|
||||
},
|
||||
|
||||
# ── Pro tier (Premium providers) ───────────────────────────────────────────
|
||||
# OpenAI
|
||||
"gpt-4o": {
|
||||
"name": "GPT-4o",
|
||||
"provider": "OpenAI",
|
||||
"badge": "Powerful",
|
||||
"description": "Most capable OpenAI model",
|
||||
},
|
||||
"gpt-4-turbo": {
|
||||
"name": "GPT-4 Turbo",
|
||||
"provider": "OpenAI",
|
||||
"badge": "Smart",
|
||||
"description": "Fast and capable with large context",
|
||||
},
|
||||
"gpt-3.5-turbo": {
|
||||
"name": "GPT-3.5 Turbo",
|
||||
"gpt-4o-mini": {
|
||||
"name": "GPT-4o Mini",
|
||||
"provider": "OpenAI",
|
||||
"badge": "Efficient",
|
||||
"description": "Cost-effective for simpler tasks",
|
||||
"description": "Fast and cost-effective OpenAI model",
|
||||
},
|
||||
"claude-3-5-sonnet-20241022": {
|
||||
"name": "Claude 3.5 Sonnet",
|
||||
# Anthropic
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"name": "Claude Haiku 4.5",
|
||||
"provider": "Anthropic",
|
||||
"badge": "Reasoning",
|
||||
"description": "Excellent at analysis and reasoning",
|
||||
"badge": "Fast",
|
||||
"description": "Fast and affordable Anthropic model",
|
||||
},
|
||||
"claude-3-opus-20240229": {
|
||||
"name": "Claude 3 Opus",
|
||||
"provider": "Anthropic",
|
||||
"badge": "Advanced",
|
||||
"description": "Most capable Anthropic model",
|
||||
},
|
||||
"gemini-1.5-pro": {
|
||||
"name": "Gemini 1.5 Pro",
|
||||
# Google Gemini
|
||||
"gemini-2.5-flash": {
|
||||
"name": "Gemini 2.5 Flash",
|
||||
"provider": "Google",
|
||||
"badge": "Long Context",
|
||||
"description": "Handles very long documents well",
|
||||
"badge": "Fast",
|
||||
"description": "Fast and efficient Google model",
|
||||
},
|
||||
"gemini-2.5-lite": {
|
||||
"name": "Gemini 2.5 Lite",
|
||||
"provider": "Google",
|
||||
"badge": "Lightweight",
|
||||
"description": "Cost-effective Google model",
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"name": "Gemini 2.5 Pro",
|
||||
"provider": "Google",
|
||||
"badge": "Advanced",
|
||||
"description": "Most capable Google model with long context",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -136,86 +139,133 @@ MODEL_CATALOG = {
|
||||
# ─── Model ID → LLM provider mapping (used by llm_client.py for routing) ─────
|
||||
|
||||
MODEL_PROVIDERS = {
|
||||
# Fireworks
|
||||
"accounts/fireworks/models/llama-v3p3-70b-instruct": "fireworks",
|
||||
"accounts/fireworks/models/qwen3-235b-a22b": "fireworks",
|
||||
"accounts/fireworks/models/deepseek-v3p1": "fireworks",
|
||||
"accounts/fireworks/models/kimi-k2-instruct-0905": "fireworks",
|
||||
"accounts/fireworks/models/llama-v3p1-70b-instruct": "fireworks",
|
||||
"accounts/fireworks/models/mixtral-8x7b-instruct": "fireworks",
|
||||
"accounts/fireworks/models/qwen2p5-72b-instruct": "fireworks",
|
||||
# OpenAI
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"gpt-3.5-turbo": "openai",
|
||||
"claude-3-5-sonnet-20241022": "anthropic",
|
||||
"claude-3-opus-20240229": "anthropic",
|
||||
"gemini-1.5-pro": "google",
|
||||
"gpt-4o-mini": "openai",
|
||||
# Anthropic
|
||||
"claude-haiku-4-5-20251001": "anthropic",
|
||||
# Google
|
||||
"gemini-2.5-flash": "google",
|
||||
"gemini-2.5-lite": "google",
|
||||
"gemini-2.5-pro": "google",
|
||||
}
|
||||
|
||||
|
||||
# ─── Default model per plan (pre-selected in the frontend) ────────────────────
|
||||
|
||||
DEFAULT_MODELS = {
|
||||
"free": "accounts/fireworks/models/kimi-k2-instruct-0905",
|
||||
"starter": "accounts/fireworks/models/llama-v3p1-70b-instruct",
|
||||
"free": "accounts/fireworks/models/llama-v3p3-70b-instruct",
|
||||
"starter": "accounts/fireworks/models/qwen3-235b-a22b",
|
||||
"pro": "gpt-4o",
|
||||
"enterprise": "gpt-4o",
|
||||
}
|
||||
|
||||
|
||||
# ─── Plan limits ──────────────────────────────────────────────────────────────
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PLAN LIMITS — Pricing: Starter $3/mo, Pro $20/mo
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Cost analysis (per 1M tokens approx):
|
||||
# Fireworks Llama 3.3 70B: $0.90/M
|
||||
# Fireworks Qwen3 235B: $0.22 in / $0.88 out
|
||||
# Fireworks DeepSeek V3.1: $0.56 in / $1.68 out
|
||||
# Fireworks Kimi K2: $0.60 in / $2.50 out
|
||||
# GPT-4o: $2.50 in / $10.00 out
|
||||
# GPT-4o Mini: $0.15 in / $0.60 out
|
||||
# Claude Haiku 4.5: $0.80 in / $4.00 out
|
||||
# Gemini 2.5 Flash: ~$0.15 in / $0.60 out
|
||||
# Gemini 2.5 Lite: ~$0.075 in / $0.30 out
|
||||
# Gemini 2.5 Pro: ~$1.25 in / $10.00 out
|
||||
#
|
||||
# Avg conversation: ~2K tokens input + 1K output = ~3K tokens
|
||||
# Fireworks models: ~$0.001-$0.004 per conversation
|
||||
# GPT-4o: ~$0.015 per conversation
|
||||
# GPT-4o Mini: ~$0.001 per conversation
|
||||
# Claude Haiku: ~$0.006 per conversation
|
||||
# Gemini Flash: ~$0.001 per conversation
|
||||
# Gemini Pro: ~$0.013 per conversation
|
||||
#
|
||||
# Starter at $3/mo with 500 convos: max cost ~$2/mo (fireworks) → margin OK
|
||||
# Pro at $20/mo with 2,000 convos: max cost ~$12/mo (if all GPT-4o) → margin OK
|
||||
# Typical mix: ~$5-8/mo actual cost → healthy margin
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PLAN_LIMITS = {
|
||||
"free": {
|
||||
"max_chatbots": 999999, # unlimited creation
|
||||
"max_published": 0, # cannot publish
|
||||
"max_published": 0, # cannot publish
|
||||
"max_documents_per_chatbot": 3,
|
||||
"max_document_size_mb": 5,
|
||||
"models": [
|
||||
"accounts/fireworks/models/kimi-k2-instruct-0905",
|
||||
"accounts/fireworks/models/llama-v3p3-70b-instruct",
|
||||
],
|
||||
"conversations_limit": 999999, # unlimited preview
|
||||
"conversations_limit": 50, # 50 preview conversations/month
|
||||
"code_export": False,
|
||||
"analytics": False,
|
||||
"features": ["preview_mode", "testing"],
|
||||
},
|
||||
"starter": {
|
||||
"max_chatbots": 999999,
|
||||
"max_published": 1,
|
||||
"max_documents_per_chatbot": 10,
|
||||
"max_document_size_mb": 10,
|
||||
"models": [
|
||||
"accounts/fireworks/models/llama-v3p3-70b-instruct",
|
||||
"accounts/fireworks/models/qwen3-235b-a22b",
|
||||
"accounts/fireworks/models/deepseek-v3p1",
|
||||
"accounts/fireworks/models/kimi-k2-instruct-0905",
|
||||
"accounts/fireworks/models/llama-v3p1-70b-instruct",
|
||||
"accounts/fireworks/models/mixtral-8x7b-instruct",
|
||||
"accounts/fireworks/models/qwen2p5-72b-instruct",
|
||||
],
|
||||
"conversations_limit": 5000,
|
||||
"conversations_limit": 500, # 500 conversations/month
|
||||
"code_export": False,
|
||||
"analytics": True,
|
||||
"features": ["marketplace", "analytics", "branding"],
|
||||
},
|
||||
"pro": {
|
||||
"max_chatbots": 3,
|
||||
"max_published": 3,
|
||||
"max_chatbots": 5,
|
||||
"max_published": 5,
|
||||
"max_documents_per_chatbot": 50,
|
||||
"max_document_size_mb": 50,
|
||||
"models": [
|
||||
# Fireworks (included)
|
||||
"accounts/fireworks/models/llama-v3p3-70b-instruct",
|
||||
"accounts/fireworks/models/qwen3-235b-a22b",
|
||||
"accounts/fireworks/models/deepseek-v3p1",
|
||||
"accounts/fireworks/models/kimi-k2-instruct-0905",
|
||||
"accounts/fireworks/models/llama-v3p1-70b-instruct",
|
||||
"accounts/fireworks/models/mixtral-8x7b-instruct",
|
||||
# OpenAI
|
||||
"gpt-4o",
|
||||
"gpt-4-turbo",
|
||||
"gpt-3.5-turbo",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-opus-20240229",
|
||||
"gemini-1.5-pro",
|
||||
"gpt-4o-mini",
|
||||
# Anthropic
|
||||
"claude-haiku-4-5-20251001",
|
||||
# Google
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-lite",
|
||||
"gemini-2.5-pro",
|
||||
],
|
||||
"conversations_limit": 20000,
|
||||
"conversations_limit": 2000, # 2,000 conversations/month
|
||||
"code_export": True,
|
||||
"analytics": True,
|
||||
"features": [
|
||||
"marketplace",
|
||||
"code_export",
|
||||
"advanced_analytics",
|
||||
"priority_support",
|
||||
"custom_domain",
|
||||
"ab_testing",
|
||||
],
|
||||
},
|
||||
"enterprise": {
|
||||
"max_chatbots": 999999,
|
||||
"max_published": 999999,
|
||||
"max_documents_per_chatbot": 999999,
|
||||
"max_document_size_mb": 200,
|
||||
"models": ["*"], # resolves to all MODEL_CATALOG keys
|
||||
"conversations_limit": 999999,
|
||||
"code_export": True,
|
||||
"analytics": True,
|
||||
"features": ["*"],
|
||||
},
|
||||
}
|
||||
@@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse
|
||||
import logging
|
||||
|
||||
from app.config import settings
|
||||
from app.routers import auth, chatbots, documents, chat, marketplace, billing, models
|
||||
from app.routers import auth, chatbots, documents, chat, marketplace, billing, models, analytics
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
@@ -15,15 +15,12 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# BUG-13 FIX: Replace deprecated @app.on_event("startup") with lifespan
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
logger.info("Contexta API starting up...")
|
||||
logger.info(f"Environment: {settings.app_env}")
|
||||
logger.info(f"Allowed origins: {settings.allowed_origins_list}")
|
||||
yield
|
||||
# Shutdown
|
||||
logger.info("Contexta API shutting down...")
|
||||
|
||||
|
||||
@@ -54,6 +51,7 @@ app.include_router(chat.router, prefix="/api/v1")
|
||||
app.include_router(marketplace.router, prefix="/api/v1")
|
||||
app.include_router(billing.router, prefix="/api/v1")
|
||||
app.include_router(models.router, prefix="/api/v1")
|
||||
app.include_router(analytics.router, prefix="/api/v1")
|
||||
|
||||
# ── Health & Info ──────────────────────────────────────────────────────────────
|
||||
@app.get("/")
|
||||
|
||||
364
app/routers/analytics.py
Normal file
364
app/routers/analytics.py
Normal 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,
|
||||
)
|
||||
@@ -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) \
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user