diff --git a/app/config.py b/app/config.py index b7d5989..a68d474 100644 --- a/app/config.py +++ b/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": ["*"], }, } \ No newline at end of file diff --git a/app/main.py b/app/main.py index 5db3c5a..6c8708f 100644 --- a/app/main.py +++ b/app/main.py @@ -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("/") diff --git a/app/routers/analytics.py b/app/routers/analytics.py new file mode 100644 index 0000000..3551e85 --- /dev/null +++ b/app/routers/analytics.py @@ -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, + ) diff --git a/app/routers/chat.py b/app/routers/chat.py index c646ffa..88eedfe 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -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) \ diff --git a/app/routers/marketplace.py b/app/routers/marketplace.py index 23a6289..1b712cc 100644 --- a/app/routers/marketplace.py +++ b/app/routers/marketplace.py @@ -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", ]