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

View File

@@ -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": ["*"],
},
}

View File

@@ -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
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",
]