mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
Initial commit
This commit is contained in:
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
133
app/routers/auth.py
Normal file
133
app/routers/auth.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from app.models import UserSignup, UserLogin, UserResponse, TokenResponse
|
||||
from app.database import get_supabase
|
||||
from app.dependencies import get_current_user
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
|
||||
@router.post("/signup", response_model=TokenResponse)
|
||||
async def signup(data: UserSignup):
|
||||
supabase = get_supabase()
|
||||
try:
|
||||
# Create auth user
|
||||
auth_resp = supabase.auth.sign_up(
|
||||
{"email": data.email, "password": data.password}
|
||||
)
|
||||
if not auth_resp.user:
|
||||
raise HTTPException(status_code=400, detail="Failed to create account")
|
||||
|
||||
user = auth_resp.user
|
||||
|
||||
# Create company record
|
||||
supabase.table("companies").insert(
|
||||
{
|
||||
"owner_id": user.id,
|
||||
"name": data.company_name,
|
||||
}
|
||||
).execute()
|
||||
|
||||
# Create free subscription
|
||||
supabase.table("subscriptions").insert(
|
||||
{
|
||||
"user_id": user.id,
|
||||
"plan": "free",
|
||||
"status": "active",
|
||||
}
|
||||
).execute()
|
||||
|
||||
token = auth_resp.session.access_token if auth_resp.session else ""
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
user=UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
company_name=data.company_name,
|
||||
plan="free",
|
||||
),
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Signup error: {e}")
|
||||
if "already registered" in str(e).lower() or "already exists" in str(e).lower():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(data: UserLogin):
|
||||
supabase = get_supabase()
|
||||
try:
|
||||
auth_resp = supabase.auth.sign_in_with_password(
|
||||
{"email": data.email, "password": data.password}
|
||||
)
|
||||
if not auth_resp.user or not auth_resp.session:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
user = auth_resp.user
|
||||
|
||||
# Get company info
|
||||
company = supabase.table("companies").select("name").eq("owner_id", user.id).execute()
|
||||
company_name = company.data[0]["name"] if company.data else ""
|
||||
|
||||
# Get subscription
|
||||
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"
|
||||
|
||||
return TokenResponse(
|
||||
access_token=auth_resp.session.access_token,
|
||||
user=UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
company_name=company_name,
|
||||
plan=plan,
|
||||
),
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Login error: {e}")
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(user=Depends(get_current_user)):
|
||||
supabase = get_supabase()
|
||||
try:
|
||||
supabase.auth.sign_out()
|
||||
except Exception:
|
||||
pass
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_me(user=Depends(get_current_user)):
|
||||
supabase = get_supabase()
|
||||
|
||||
company = supabase.table("companies").select("name").eq("owner_id", user.id).execute()
|
||||
company_name = company.data[0]["name"] if company.data else ""
|
||||
|
||||
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"
|
||||
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
company_name=company_name,
|
||||
plan=plan,
|
||||
)
|
||||
187
app/routers/billing.py
Normal file
187
app/routers/billing.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request, Header
|
||||
from app.models import CheckoutSessionCreate, CheckoutSessionResponse, SubscriptionResponse
|
||||
from app.database import get_supabase
|
||||
from app.dependencies import get_current_user
|
||||
from app.config import settings
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/billing", tags=["Billing"])
|
||||
|
||||
PLAN_PRICE_IDS = {
|
||||
"starter": settings.stripe_starter_price_id,
|
||||
"pro": settings.stripe_pro_price_id,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/checkout", response_model=CheckoutSessionResponse)
|
||||
async def create_checkout_session(data: CheckoutSessionCreate, user=Depends(get_current_user)):
|
||||
try:
|
||||
import stripe
|
||||
stripe.api_key = settings.stripe_secret_key
|
||||
|
||||
if data.plan not in PLAN_PRICE_IDS:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid plan: {data.plan}")
|
||||
|
||||
price_id = PLAN_PRICE_IDS[data.plan]
|
||||
if not price_id:
|
||||
raise HTTPException(status_code=400, detail="Plan price not configured")
|
||||
|
||||
supabase = get_supabase()
|
||||
sub = supabase.table("subscriptions").select("stripe_customer_id").eq("user_id", user.id).execute()
|
||||
|
||||
customer_id = None
|
||||
if sub.data and sub.data[0].get("stripe_customer_id"):
|
||||
customer_id = sub.data[0]["stripe_customer_id"]
|
||||
else:
|
||||
customer = stripe.Customer.create(email=user.email)
|
||||
customer_id = customer.id
|
||||
|
||||
session = stripe.checkout.Session.create(
|
||||
customer=customer_id,
|
||||
payment_method_types=["card"],
|
||||
line_items=[{"price": price_id, "quantity": 1}],
|
||||
mode="subscription",
|
||||
success_url=data.success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url=data.cancel_url,
|
||||
metadata={"user_id": user.id, "plan": data.plan},
|
||||
)
|
||||
|
||||
return CheckoutSessionResponse(checkout_url=session.url, session_id=session.id)
|
||||
|
||||
except ImportError:
|
||||
raise HTTPException(status_code=500, detail="Stripe not configured")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Checkout error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/webhook")
|
||||
async def stripe_webhook(
|
||||
request: Request,
|
||||
stripe_signature: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
import stripe
|
||||
|
||||
stripe.api_key = settings.stripe_secret_key
|
||||
payload = await request.body()
|
||||
|
||||
if settings.stripe_webhook_secret and stripe_signature:
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, stripe_signature, settings.stripe_webhook_secret
|
||||
)
|
||||
except stripe.error.SignatureVerificationError:
|
||||
raise HTTPException(status_code=400, detail="Invalid signature")
|
||||
else:
|
||||
import json
|
||||
event = json.loads(payload)
|
||||
|
||||
supabase = get_supabase()
|
||||
event_type = event.get("type", "")
|
||||
|
||||
if event_type == "checkout.session.completed":
|
||||
session = event["data"]["object"]
|
||||
user_id = session.get("metadata", {}).get("user_id")
|
||||
plan = session.get("metadata", {}).get("plan", "starter")
|
||||
customer_id = session.get("customer")
|
||||
subscription_id = session.get("subscription")
|
||||
|
||||
if user_id:
|
||||
supabase.table("subscriptions").upsert({
|
||||
"user_id": user_id,
|
||||
"plan": plan,
|
||||
"status": "active",
|
||||
"stripe_customer_id": customer_id,
|
||||
"stripe_subscription_id": subscription_id,
|
||||
}, on_conflict="user_id").execute()
|
||||
|
||||
elif event_type in ("customer.subscription.updated", "customer.subscription.deleted"):
|
||||
sub_obj = event["data"]["object"]
|
||||
customer_id = sub_obj.get("customer")
|
||||
status = sub_obj.get("status", "canceled")
|
||||
|
||||
existing = supabase.table("subscriptions").select("*").eq("stripe_customer_id", customer_id).execute()
|
||||
if existing.data:
|
||||
mapped_status = "active" if status in ("active", "trialing") else "canceled"
|
||||
supabase.table("subscriptions").update({
|
||||
"status": mapped_status,
|
||||
}).eq("stripe_customer_id", customer_id).execute()
|
||||
|
||||
# Unpublish chatbots if subscription canceled
|
||||
if mapped_status == "canceled":
|
||||
user_id = existing.data[0]["user_id"]
|
||||
company = supabase.table("companies").select("id").eq("owner_id", user_id).execute()
|
||||
if company.data:
|
||||
supabase.table("chatbots").update({
|
||||
"is_published": False,
|
||||
"visibility": "preview",
|
||||
}).eq("company_id", company.data[0]["id"]).execute()
|
||||
|
||||
return {"received": True}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Webhook error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/subscription", response_model=SubscriptionResponse)
|
||||
async def get_subscription(user=Depends(get_current_user)):
|
||||
supabase = get_supabase()
|
||||
result = supabase.table("subscriptions").select("*").eq("user_id", user.id).execute()
|
||||
|
||||
if not result.data:
|
||||
return SubscriptionResponse(
|
||||
id="free",
|
||||
user_id=user.id,
|
||||
plan="free",
|
||||
status="active",
|
||||
)
|
||||
|
||||
sub = result.data[0]
|
||||
return SubscriptionResponse(
|
||||
id=sub["id"],
|
||||
user_id=sub["user_id"],
|
||||
plan=sub["plan"],
|
||||
status=sub["status"],
|
||||
stripe_customer_id=sub.get("stripe_customer_id"),
|
||||
current_period_start=sub.get("current_period_start"),
|
||||
current_period_end=sub.get("current_period_end"),
|
||||
chatbots_published=sub.get("chatbots_published", 0),
|
||||
conversations_used=sub.get("conversations_used", 0),
|
||||
created_at=sub.get("created_at"),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/portal")
|
||||
async def customer_portal(request: Request, user=Depends(get_current_user)):
|
||||
"""Create Stripe customer portal session"""
|
||||
try:
|
||||
import stripe
|
||||
stripe.api_key = settings.stripe_secret_key
|
||||
|
||||
supabase = get_supabase()
|
||||
sub = supabase.table("subscriptions").select("stripe_customer_id").eq("user_id", user.id).execute()
|
||||
|
||||
if not sub.data or not sub.data[0].get("stripe_customer_id"):
|
||||
raise HTTPException(status_code=404, detail="No subscription found")
|
||||
|
||||
body = await request.json()
|
||||
return_url = body.get("return_url", "http://localhost:5173/settings")
|
||||
|
||||
session = stripe.billing_portal.Session.create(
|
||||
customer=sub.data[0]["stripe_customer_id"],
|
||||
return_url=return_url,
|
||||
)
|
||||
return {"url": session.url}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
212
app/routers/chat.py
Normal file
212
app/routers/chat.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from app.models import ChatMessage, ChatResponse, ConversationResponse, MessageResponse
|
||||
from app.database import get_supabase
|
||||
from app.dependencies import get_current_user, get_optional_user
|
||||
from app.services.rag import rag_engine
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
|
||||
def _get_public_chatbot(chatbot_id: str, supabase) -> dict:
|
||||
"""Get a published chatbot (or any chatbot for preview)"""
|
||||
result = supabase.table("chatbots").select("*, companies(name, logo_url)").eq("id", chatbot_id).execute()
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=404, detail="Chatbot not found")
|
||||
return result.data[0]
|
||||
|
||||
|
||||
@router.post("/chat/{chatbot_id}", response_model=ChatResponse)
|
||||
async def chat(
|
||||
chatbot_id: str,
|
||||
message: ChatMessage,
|
||||
user=Depends(get_optional_user),
|
||||
):
|
||||
supabase = get_supabase()
|
||||
chatbot = _get_public_chatbot(chatbot_id, supabase)
|
||||
|
||||
# Allow preview access for owner, require published for public
|
||||
if not chatbot.get("is_published"):
|
||||
if not user:
|
||||
raise HTTPException(status_code=403, detail="This chatbot is in preview mode")
|
||||
# Check ownership
|
||||
company = supabase.table("companies").select("id").eq("owner_id", user.id).execute()
|
||||
if not company.data or company.data[0]["id"] != chatbot.get("company_id"):
|
||||
raise HTTPException(status_code=403, detail="This chatbot is in preview mode")
|
||||
|
||||
collection_name = chatbot.get("qdrant_collection_name")
|
||||
if not collection_name:
|
||||
raise HTTPException(status_code=400, detail="Chatbot has no knowledge base configured")
|
||||
|
||||
# Get or create conversation
|
||||
session_id = message.session_id or str(uuid.uuid4())
|
||||
conversation = _get_or_create_conversation(
|
||||
chatbot_id=chatbot_id,
|
||||
session_id=session_id,
|
||||
user_id=user.id if user else None,
|
||||
language=message.language,
|
||||
supabase=supabase,
|
||||
)
|
||||
|
||||
# Get conversation history
|
||||
history = _get_conversation_history(conversation["id"], supabase)
|
||||
|
||||
# Get company info for context
|
||||
company_data = chatbot.get("companies", {}) or {}
|
||||
chatbot_config = {
|
||||
**chatbot,
|
||||
"company_name": company_data.get("name", ""),
|
||||
}
|
||||
|
||||
# Run RAG
|
||||
result = await rag_engine.process_query(
|
||||
query=message.message,
|
||||
collection_name=collection_name,
|
||||
chatbot_config=chatbot_config,
|
||||
conversation_history=history,
|
||||
language=message.language,
|
||||
)
|
||||
|
||||
# Save messages
|
||||
_save_message(conversation["id"], "user", message.message, supabase)
|
||||
_save_message(
|
||||
conversation["id"],
|
||||
"assistant",
|
||||
result["response"],
|
||||
supabase,
|
||||
sources=[s.model_dump() for s in result.get("sources", [])],
|
||||
model=result.get("model", ""),
|
||||
)
|
||||
|
||||
# Update conversation message count
|
||||
supabase.table("conversations").update({
|
||||
"message_count": len(history) + 2
|
||||
}).eq("id", conversation["id"]).execute()
|
||||
|
||||
return ChatResponse(
|
||||
response=result["response"],
|
||||
session_id=session_id,
|
||||
sources=result.get("sources", []),
|
||||
model_used=result.get("model", ""),
|
||||
tokens_used=result.get("tokens_used", 0),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/chat/{chatbot_id}/history/{session_id}", response_model=List[MessageResponse])
|
||||
async def get_chat_history(
|
||||
chatbot_id: str,
|
||||
session_id: str,
|
||||
user=Depends(get_optional_user),
|
||||
):
|
||||
supabase = get_supabase()
|
||||
|
||||
conversation = supabase.table("conversations").select("*") \
|
||||
.eq("chatbot_id", chatbot_id) \
|
||||
.eq("session_id", session_id) \
|
||||
.execute()
|
||||
|
||||
if not conversation.data:
|
||||
return []
|
||||
|
||||
conv_id = conversation.data[0]["id"]
|
||||
messages = supabase.table("messages").select("*") \
|
||||
.eq("conversation_id", conv_id) \
|
||||
.order("created_at", asc=True) \
|
||||
.execute()
|
||||
|
||||
return [
|
||||
MessageResponse(
|
||||
id=m["id"],
|
||||
role=m["role"],
|
||||
content=m["content"],
|
||||
sources=m.get("sources"),
|
||||
created_at=m.get("created_at"),
|
||||
)
|
||||
for m in (messages.data or [])
|
||||
]
|
||||
|
||||
|
||||
# ── 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,
|
||||
}
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_or_create_conversation(
|
||||
chatbot_id: str,
|
||||
session_id: str,
|
||||
user_id: Optional[str],
|
||||
language: str,
|
||||
supabase,
|
||||
) -> dict:
|
||||
existing = supabase.table("conversations").select("*") \
|
||||
.eq("chatbot_id", chatbot_id) \
|
||||
.eq("session_id", session_id) \
|
||||
.execute()
|
||||
|
||||
if existing.data:
|
||||
return existing.data[0]
|
||||
|
||||
new_conv = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"chatbot_id": chatbot_id,
|
||||
"user_id": user_id,
|
||||
"session_id": session_id,
|
||||
"language": language,
|
||||
"message_count": 0,
|
||||
}
|
||||
result = supabase.table("conversations").insert(new_conv).execute()
|
||||
return result.data[0]
|
||||
|
||||
|
||||
def _get_conversation_history(conversation_id: str, supabase) -> List[dict]:
|
||||
messages = supabase.table("messages").select("role, content") \
|
||||
.eq("conversation_id", conversation_id) \
|
||||
.order("created_at", asc=True) \
|
||||
.limit(20) \
|
||||
.execute()
|
||||
return messages.data or []
|
||||
|
||||
|
||||
def _save_message(
|
||||
conversation_id: str,
|
||||
role: str,
|
||||
content: str,
|
||||
supabase,
|
||||
sources: Optional[list] = None,
|
||||
model: str = "",
|
||||
):
|
||||
supabase.table("messages").insert({
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversation_id": conversation_id,
|
||||
"role": role,
|
||||
"content": content,
|
||||
"sources": sources,
|
||||
"model": model,
|
||||
}).execute()
|
||||
240
app/routers/chatbots.py
Normal file
240
app/routers/chatbots.py
Normal file
@@ -0,0 +1,240 @@
|
||||
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.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}")
|
||||
# Continue without vector store for now
|
||||
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,
|
||||
"category": data.category,
|
||||
"industry": data.industry,
|
||||
"languages": data.languages,
|
||||
"visibility": "preview",
|
||||
"is_published": False,
|
||||
"qdrant_collection_name": collection_name,
|
||||
}
|
||||
|
||||
result = supabase.table("chatbots").insert(chatbot_data).execute()
|
||||
if not result.data:
|
||||
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 collection: {e}")
|
||||
|
||||
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 ("pro", "enterprise"):
|
||||
raise HTTPException(status_code=402, detail="Code export requires Pro 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}"},
|
||||
)
|
||||
|
||||
|
||||
# ── 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/llama-v3p1-70b-instruct"),
|
||||
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?"),
|
||||
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"),
|
||||
)
|
||||
208
app/routers/documents.py
Normal file
208
app/routers/documents.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks
|
||||
from app.models import DocumentResponse, SuccessResponse
|
||||
from app.database import get_supabase
|
||||
from app.dependencies import get_current_user
|
||||
from app.services.document_processor import process_document
|
||||
from app.services.embeddings import embedding_service
|
||||
from app.services.vector_store import vector_store
|
||||
from app.config import settings
|
||||
from typing import List
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/chatbots/{chatbot_id}/documents", tags=["Documents"])
|
||||
|
||||
ALLOWED_TYPES = {
|
||||
"application/pdf": ".pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||
"text/csv": ".csv",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||
"text/plain": ".txt",
|
||||
"text/markdown": ".md",
|
||||
}
|
||||
|
||||
ALLOWED_EXTENSIONS = {".pdf", ".docx", ".csv", ".xlsx", ".txt", ".md"}
|
||||
|
||||
|
||||
def _get_user_chatbot(chatbot_id: str, user_id: str, supabase) -> dict:
|
||||
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"]
|
||||
|
||||
chatbot = supabase.table("chatbots").select("*").eq("id", chatbot_id).eq("company_id", company_id).execute()
|
||||
if not chatbot.data:
|
||||
raise HTTPException(status_code=404, detail="Chatbot not found")
|
||||
return chatbot.data[0]
|
||||
|
||||
|
||||
@router.post("", response_model=DocumentResponse, status_code=201)
|
||||
async def upload_document(
|
||||
chatbot_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
supabase = get_supabase()
|
||||
chatbot = _get_user_chatbot(chatbot_id, user.id, supabase)
|
||||
|
||||
# Validate file
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No filename provided")
|
||||
|
||||
ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File type not supported. Allowed: {', '.join(ALLOWED_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
# Read file
|
||||
file_bytes = await file.read()
|
||||
file_size = len(file_bytes)
|
||||
max_size = settings.max_file_size_mb * 1024 * 1024
|
||||
|
||||
if file_size > max_size:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"File too large. Max size: {settings.max_file_size_mb}MB"
|
||||
)
|
||||
|
||||
# Create document record
|
||||
doc_id = str(uuid.uuid4())
|
||||
doc_data = {
|
||||
"id": doc_id,
|
||||
"chatbot_id": chatbot_id,
|
||||
"file_name": file.filename,
|
||||
"file_type": ext,
|
||||
"file_size": file_size,
|
||||
"chunk_count": 0,
|
||||
"status": "processing",
|
||||
}
|
||||
|
||||
result = supabase.table("documents").insert(doc_data).execute()
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to create document record")
|
||||
|
||||
# Process in background
|
||||
background_tasks.add_task(
|
||||
_process_document_bg,
|
||||
file_bytes=file_bytes,
|
||||
file_name=file.filename,
|
||||
doc_id=doc_id,
|
||||
chatbot=chatbot,
|
||||
supabase=supabase,
|
||||
)
|
||||
|
||||
return DocumentResponse(**result.data[0])
|
||||
|
||||
|
||||
async def _process_document_bg(
|
||||
file_bytes: bytes,
|
||||
file_name: str,
|
||||
doc_id: str,
|
||||
chatbot: dict,
|
||||
supabase,
|
||||
):
|
||||
"""Background task to process and embed a document"""
|
||||
try:
|
||||
company_id = chatbot.get("company_id", "")
|
||||
collection_name = chatbot.get("qdrant_collection_name")
|
||||
|
||||
if not collection_name:
|
||||
logger.error(f"No Qdrant collection for chatbot {chatbot['id']}")
|
||||
supabase.table("documents").update({
|
||||
"status": "failed",
|
||||
"error_message": "Vector store not configured"
|
||||
}).eq("id", doc_id).execute()
|
||||
return
|
||||
|
||||
# Ensure collection exists
|
||||
if not vector_store.collection_exists(collection_name):
|
||||
vector_store.create_collection(collection_name)
|
||||
|
||||
# Process document
|
||||
chunks, payloads = process_document(
|
||||
file_bytes=file_bytes,
|
||||
file_name=file_name,
|
||||
document_id=doc_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
|
||||
if not chunks:
|
||||
raise ValueError("No text extracted from document")
|
||||
|
||||
# Generate embeddings in batches
|
||||
batch_size = 50
|
||||
all_ids = []
|
||||
all_vectors = []
|
||||
all_payloads = []
|
||||
|
||||
for i in range(0, len(chunks), batch_size):
|
||||
batch_chunks = chunks[i:i + batch_size]
|
||||
batch_payloads = payloads[i:i + batch_size]
|
||||
|
||||
vectors = embedding_service.embed_batch(batch_chunks)
|
||||
ids = [str(uuid.uuid4()) for _ in vectors]
|
||||
|
||||
all_ids.extend(ids)
|
||||
all_vectors.extend(vectors)
|
||||
all_payloads.extend(batch_payloads)
|
||||
|
||||
# Upsert to Qdrant
|
||||
vector_store.upsert_vectors(
|
||||
collection_name=collection_name,
|
||||
vectors=all_vectors,
|
||||
payloads=all_payloads,
|
||||
ids=all_ids,
|
||||
)
|
||||
|
||||
# Update document record
|
||||
supabase.table("documents").update({
|
||||
"status": "completed",
|
||||
"chunk_count": len(chunks),
|
||||
}).eq("id", doc_id).execute()
|
||||
|
||||
logger.info(f"Document {doc_id} processed: {len(chunks)} chunks")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Document processing error for {doc_id}: {e}")
|
||||
supabase.table("documents").update({
|
||||
"status": "failed",
|
||||
"error_message": str(e)[:500],
|
||||
}).eq("id", doc_id).execute()
|
||||
|
||||
|
||||
@router.get("", response_model=List[DocumentResponse])
|
||||
async def list_documents(chatbot_id: str, user=Depends(get_current_user)):
|
||||
supabase = get_supabase()
|
||||
_get_user_chatbot(chatbot_id, user.id, supabase)
|
||||
|
||||
result = supabase.table("documents").select("*") \
|
||||
.eq("chatbot_id", chatbot_id) \
|
||||
.order("created_at", desc=True) \
|
||||
.execute()
|
||||
|
||||
return [DocumentResponse(**d) for d in (result.data or [])]
|
||||
|
||||
|
||||
@router.delete("/{document_id}", response_model=SuccessResponse)
|
||||
async def delete_document(chatbot_id: str, document_id: str, user=Depends(get_current_user)):
|
||||
supabase = get_supabase()
|
||||
chatbot = _get_user_chatbot(chatbot_id, user.id, supabase)
|
||||
|
||||
doc = supabase.table("documents").select("*").eq("id", document_id).eq("chatbot_id", chatbot_id).execute()
|
||||
if not doc.data:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
# Remove vectors from Qdrant
|
||||
collection_name = chatbot.get("qdrant_collection_name")
|
||||
if collection_name:
|
||||
try:
|
||||
vector_store.delete_by_document_id(collection_name, document_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete vectors: {e}")
|
||||
|
||||
supabase.table("documents").delete().eq("id", document_id).execute()
|
||||
return SuccessResponse(success=True, message="Document deleted")
|
||||
133
app/routers/marketplace.py
Normal file
133
app/routers/marketplace.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from app.models import ChatbotPublicResponse, MarketplaceResponse, RatingCreate
|
||||
from app.database import get_supabase
|
||||
from app.dependencies import get_optional_user, get_current_user
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/marketplace", tags=["Marketplace"])
|
||||
|
||||
CATEGORIES = [
|
||||
"Customer Support", "Sales", "FAQ", "E-commerce",
|
||||
"Healthcare", "Finance", "Education", "HR", "Legal", "Other"
|
||||
]
|
||||
|
||||
INDUSTRIES = [
|
||||
"Technology", "E-commerce", "Healthcare", "Finance",
|
||||
"Education", "Legal", "Real Estate", "Hospitality", "Retail", "Other"
|
||||
]
|
||||
|
||||
|
||||
@router.get("/chatbots", response_model=MarketplaceResponse)
|
||||
async def list_marketplace_chatbots(
|
||||
category: Optional[str] = Query(None),
|
||||
industry: Optional[str] = Query(None),
|
||||
language: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
user=Depends(get_optional_user),
|
||||
):
|
||||
supabase = get_supabase()
|
||||
|
||||
query = supabase.table("chatbots").select(
|
||||
"*, companies(name, logo_url)"
|
||||
).eq("is_published", True).eq("visibility", "published")
|
||||
|
||||
if category:
|
||||
query = query.eq("category", category)
|
||||
if industry:
|
||||
query = query.eq("industry", industry)
|
||||
if search:
|
||||
query = query.ilike("name", f"%{search}%")
|
||||
|
||||
offset = (page - 1) * limit
|
||||
result = query.order("created_at", desc=True).range(offset, offset + limit - 1).execute()
|
||||
all_result = supabase.table("chatbots").select("id", count="exact").eq("is_published", True).execute()
|
||||
total = all_result.count or 0
|
||||
|
||||
chatbots = []
|
||||
for c in (result.data or []):
|
||||
company_data = c.get("companies") or {}
|
||||
chatbots.append(
|
||||
ChatbotPublicResponse(
|
||||
id=c["id"],
|
||||
name=c["name"],
|
||||
description=c.get("description"),
|
||||
category=c.get("category"),
|
||||
industry=c.get("industry"),
|
||||
languages=c.get("languages", ["en"]),
|
||||
primary_color=c.get("primary_color", "#6366f1"),
|
||||
welcome_message=c.get("welcome_message", "Hello!"),
|
||||
average_rating=c.get("average_rating"),
|
||||
total_conversations=c.get("total_conversations", 0),
|
||||
company_name=company_data.get("name"),
|
||||
company_logo=company_data.get("logo_url"),
|
||||
created_at=c.get("created_at"),
|
||||
published_at=c.get("published_at"),
|
||||
)
|
||||
)
|
||||
|
||||
return MarketplaceResponse(
|
||||
chatbots=chatbots,
|
||||
total=total,
|
||||
page=page,
|
||||
limit=limit,
|
||||
has_more=(offset + limit) < total,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/chatbots/{chatbot_id}", response_model=ChatbotPublicResponse)
|
||||
async def get_marketplace_chatbot(chatbot_id: str):
|
||||
supabase = get_supabase()
|
||||
result = supabase.table("chatbots").select("*, companies(name, logo_url)") \
|
||||
.eq("id", chatbot_id) \
|
||||
.eq("is_published", True) \
|
||||
.execute()
|
||||
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=404, detail="Chatbot not found in marketplace")
|
||||
|
||||
c = result.data[0]
|
||||
company_data = c.get("companies") or {}
|
||||
return ChatbotPublicResponse(
|
||||
id=c["id"],
|
||||
name=c["name"],
|
||||
description=c.get("description"),
|
||||
category=c.get("category"),
|
||||
industry=c.get("industry"),
|
||||
languages=c.get("languages", ["en"]),
|
||||
primary_color=c.get("primary_color", "#6366f1"),
|
||||
welcome_message=c.get("welcome_message", "Hello!"),
|
||||
average_rating=c.get("average_rating"),
|
||||
total_conversations=c.get("total_conversations", 0),
|
||||
company_name=company_data.get("name"),
|
||||
company_logo=company_data.get("logo_url"),
|
||||
created_at=c.get("created_at"),
|
||||
published_at=c.get("published_at"),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_categories():
|
||||
return {"categories": CATEGORIES, "industries": INDUSTRIES}
|
||||
|
||||
|
||||
@router.post("/chatbots/{chatbot_id}/rate")
|
||||
async def rate_chatbot(
|
||||
chatbot_id: str,
|
||||
rating: RatingCreate,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
supabase = get_supabase()
|
||||
chatbot = supabase.table("chatbots").select("id, average_rating").eq("id", chatbot_id).eq("is_published", True).execute()
|
||||
if not chatbot.data:
|
||||
raise HTTPException(status_code=404, detail="Chatbot not found")
|
||||
|
||||
# Simple rating update (average)
|
||||
current = chatbot.data[0].get("average_rating") or rating.rating
|
||||
new_avg = (current + rating.rating) / 2
|
||||
|
||||
supabase.table("chatbots").update({"average_rating": round(new_avg, 1)}).eq("id", chatbot_id).execute()
|
||||
return {"message": "Rating submitted", "new_average": round(new_avg, 1)}
|
||||
Reference in New Issue
Block a user