from fastapi import APIRouter, HTTPException, Depends, status from app.models import ( ChatbotCreate, ChatbotUpdate, ChatbotResponse, SuccessResponse ) from app.database import get_supabase from app.dependencies import get_current_user, get_user_subscription from app.services.vector_store import vector_store from app.services.storage import delete_from_storage from app.config import PLAN_LIMITS from typing import List import uuid import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/chatbots", tags=["Chatbots"]) def _get_user_company(user_id: str, supabase) -> dict: result = supabase.table("companies").select("*").eq("owner_id", user_id).execute() if not result.data: raise HTTPException(status_code=404, detail="Company not found") return result.data[0] async def _check_plan_limits(user_id: str, supabase, action: str = "create"): sub = supabase.table("subscriptions").select("*").eq("user_id", user_id).eq("status", "active").execute() plan = sub.data[0]["plan"] if sub.data else "free" limits = PLAN_LIMITS[plan] if action == "publish": published = supabase.table("chatbots").select("id", count="exact") \ .eq("company_id", _get_user_company(user_id, supabase)["id"]) \ .eq("is_published", True).execute() count = published.count or 0 max_pub = limits["max_published"] if max_pub == 0: raise HTTPException(status_code=402, detail="Upgrade to publish chatbots to marketplace") if count >= max_pub: raise HTTPException( status_code=402, detail=f"Publish limit reached ({max_pub}). Upgrade to publish more chatbots." ) return plan @router.post("", response_model=ChatbotResponse, status_code=201) async def create_chatbot(data: ChatbotCreate, user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) # Create Qdrant collection collection_name = f"company_{company['id']}_{uuid.uuid4().hex[:8]}" try: vector_store.create_collection(collection_name) except Exception as e: logger.error(f"Failed to create Qdrant collection: {e}") collection_name = None chatbot_data = { "id": str(uuid.uuid4()), "company_id": company["id"], "name": data.name, "description": data.description, "system_prompt": data.system_prompt, "model": data.model, "temperature": data.temperature, "max_tokens": data.max_tokens, "primary_color": data.primary_color, "welcome_message": data.welcome_message, "logo_url": data.logo_url, "category": data.category, "industry": data.industry, "languages": data.languages, "visibility": "preview", "is_published": False, "qdrant_collection_name": collection_name, "show_branding": data.show_branding, "lead_capture_enabled": data.lead_capture_enabled, "lead_capture_fields": data.lead_capture_fields, "lead_capture_trigger": data.lead_capture_trigger, "handoff_enabled": data.handoff_enabled, "handoff_message": data.handoff_message, "handoff_email": data.handoff_email, "handoff_keywords": data.handoff_keywords, } try: result = supabase.table("chatbots").insert(chatbot_data).execute() if not result.data: raise HTTPException(status_code=500, detail="Failed to create chatbot") except HTTPException: raise except Exception as e: # Cleanup orphaned Qdrant collection if DB insert failed if collection_name: try: vector_store.delete_collection(collection_name) logger.warning(f"Cleaned up orphaned Qdrant collection {collection_name} after DB failure") except Exception: pass raise HTTPException(status_code=500, detail="Failed to create chatbot") return _format_chatbot(result.data[0], supabase) @router.get("", response_model=List[ChatbotResponse]) async def list_chatbots(user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) result = supabase.table("chatbots").select("*") \ .eq("company_id", company["id"]) \ .order("created_at", desc=True) \ .execute() return [_format_chatbot(c, supabase) for c in (result.data or [])] @router.get("/{chatbot_id}", response_model=ChatbotResponse) async def get_chatbot(chatbot_id: str, user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) chatbot = _get_owned_chatbot(chatbot_id, company["id"], supabase) return _format_chatbot(chatbot, supabase) @router.put("/{chatbot_id}", response_model=ChatbotResponse) async def update_chatbot(chatbot_id: str, data: ChatbotUpdate, user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) _get_owned_chatbot(chatbot_id, company["id"], supabase) update_data = {k: v for k, v in data.model_dump().items() if v is not None} if not update_data: raise HTTPException(status_code=400, detail="No fields to update") result = supabase.table("chatbots").update(update_data).eq("id", chatbot_id).execute() if not result.data: raise HTTPException(status_code=500, detail="Update failed") return _format_chatbot(result.data[0], supabase) @router.delete("/{chatbot_id}", response_model=SuccessResponse) async def delete_chatbot(chatbot_id: str, user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) chatbot = _get_owned_chatbot(chatbot_id, company["id"], supabase) # Delete Qdrant collection if chatbot.get("qdrant_collection_name"): try: vector_store.delete_collection(chatbot["qdrant_collection_name"]) except Exception as e: logger.warning(f"Failed to delete Qdrant collection: {e}") # Delete logo from Supabase Storage if chatbot.get("logo_url"): delete_from_storage(supabase, "logos", chatbot["logo_url"]) supabase.table("chatbots").delete().eq("id", chatbot_id).execute() return SuccessResponse(success=True, message="Chatbot deleted") @router.post("/{chatbot_id}/publish", response_model=ChatbotResponse) async def publish_chatbot(chatbot_id: str, user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) chatbot = _get_owned_chatbot(chatbot_id, company["id"], supabase) await _check_plan_limits(user.id, supabase, "publish") result = supabase.table("chatbots").update({ "is_published": True, "visibility": "published", }).eq("id", chatbot_id).execute() return _format_chatbot(result.data[0], supabase) @router.post("/{chatbot_id}/unpublish", response_model=ChatbotResponse) async def unpublish_chatbot(chatbot_id: str, user=Depends(get_current_user)): supabase = get_supabase() company = _get_user_company(user.id, supabase) _get_owned_chatbot(chatbot_id, company["id"], supabase) result = supabase.table("chatbots").update({ "is_published": False, "visibility": "preview", }).eq("id", chatbot_id).execute() return _format_chatbot(result.data[0], supabase) @router.post("/{chatbot_id}/export") async def export_chatbot(chatbot_id: str, user=Depends(get_current_user)): from fastapi.responses import StreamingResponse from app.services.code_export import generate_export_package from app.config import settings supabase = get_supabase() company = _get_user_company(user.id, supabase) chatbot = _get_owned_chatbot(chatbot_id, company["id"], supabase) # Check plan sub = supabase.table("subscriptions").select("plan").eq("user_id", user.id).eq("status", "active").execute() plan = sub.data[0]["plan"] if sub.data else "free" if plan not in ("agency", "enterprise"): raise HTTPException(status_code=402, detail="Code export requires Agency plan or higher") zip_bytes = generate_export_package( chatbot=chatbot, company=company, qdrant_url=settings.qdrant_url, qdrant_key=settings.qdrant_api_key or "", ) filename = chatbot["name"].lower().replace(" ", "-") + "-chatbot.zip" return StreamingResponse( iter([zip_bytes]), media_type="application/zip", headers={"Content-Disposition": f"attachment; filename={filename}"}, ) @router.get("/{chatbot_id}/public") async def get_chatbot_public(chatbot_id: str): """Public endpoint - returns basic info for a published chatbot (used by PublicChatPage).""" supabase = get_supabase() result = supabase.table("chatbots").select("id, name, welcome_message, primary_color, logo_url, show_branding, is_published, description").eq("id", chatbot_id).execute() if not result.data: raise HTTPException(status_code=404, detail="Chatbot not found") chatbot = result.data[0] if not chatbot.get("is_published"): raise HTTPException(status_code=404, detail="Chatbot not found or not published") return { "id": chatbot["id"], "name": chatbot["name"], "welcome_message": chatbot.get("welcome_message", "Hello! How can I help?"), "primary_color": chatbot.get("primary_color", "#6366f1"), "logo_url": chatbot.get("logo_url"), "show_branding": chatbot.get("show_branding", True), "is_published": chatbot.get("is_published", False), "description": chatbot.get("description"), } @router.get("/{chatbot_id}/embed") async def get_chatbot_embed(chatbot_id: str, user=Depends(get_current_user)): """Returns embed info including the widget script tag for a chatbot.""" from app.config import settings supabase = get_supabase() company = _get_user_company(user.id, supabase) chatbot = _get_owned_chatbot(chatbot_id, company["id"], supabase) api_url = "http://localhost:8000" # In production, read from settings app_url = settings.app_url embed_script = f'' chat_url = f"{app_url}/chat/{chatbot_id}" return { "chatbot_id": chatbot_id, "name": chatbot.get("name"), "primary_color": chatbot.get("primary_color", "#6366f1"), "welcome_message": chatbot.get("welcome_message"), "logo_url": chatbot.get("logo_url"), "show_branding": chatbot.get("show_branding", True), "embed_script": embed_script, "chat_url": chat_url, "is_published": chatbot.get("is_published", False), } # ── Helpers ─────────────────────────────────────────────────────────────────── def _get_owned_chatbot(chatbot_id: str, company_id: str, supabase) -> dict: result = supabase.table("chatbots").select("*").eq("id", chatbot_id).eq("company_id", company_id).execute() if not result.data: raise HTTPException(status_code=404, detail="Chatbot not found") return result.data[0] def _format_chatbot(chatbot: dict, supabase) -> ChatbotResponse: doc_count = supabase.table("documents").select("id", count="exact") \ .eq("chatbot_id", chatbot["id"]) \ .eq("status", "completed") \ .execute() conv_count = supabase.table("conversations").select("id", count="exact") \ .eq("chatbot_id", chatbot["id"]) \ .execute() return ChatbotResponse( id=chatbot["id"], company_id=chatbot["company_id"], name=chatbot["name"], description=chatbot.get("description"), system_prompt=chatbot.get("system_prompt"), model=chatbot.get("model", "accounts/fireworks/models/kimi-k2-instruct-0905"), temperature=chatbot.get("temperature", 0.7), max_tokens=chatbot.get("max_tokens", 1000), primary_color=chatbot.get("primary_color", "#6366f1"), welcome_message=chatbot.get("welcome_message", "Hello! How can I help?"), logo_url=chatbot.get("logo_url"), category=chatbot.get("category"), industry=chatbot.get("industry"), languages=chatbot.get("languages", ["en"]), visibility=chatbot.get("visibility", "preview"), is_published=chatbot.get("is_published", False), qdrant_collection_name=chatbot.get("qdrant_collection_name"), document_count=doc_count.count or 0, conversation_count=conv_count.count or 0, created_at=chatbot.get("created_at"), published_at=chatbot.get("published_at"), show_branding=chatbot.get("show_branding", True), lead_capture_enabled=chatbot.get("lead_capture_enabled", False), lead_capture_fields=chatbot.get("lead_capture_fields") or ["email"], lead_capture_trigger=chatbot.get("lead_capture_trigger", "after_first_message"), handoff_enabled=chatbot.get("handoff_enabled", False), handoff_message=chatbot.get("handoff_message", "I'll connect you with our team. Please wait."), handoff_email=chatbot.get("handoff_email"), handoff_keywords=chatbot.get("handoff_keywords") or ["human", "agent", "speak to someone", "talk to a person", "real person"], booking_enabled=bool(chatbot.get("booking_enabled")), )