fixed bugs

This commit is contained in:
belviskhoremk
2026-02-23 16:47:03 +00:00
parent e151c42e81
commit 07c4c55072
6 changed files with 254 additions and 41 deletions

View File

@@ -1,5 +1,6 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from typing import Optional, List from typing import List, Optional
import os
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -52,12 +53,121 @@ class Settings(BaseSettings):
settings = Settings() settings = Settings()
# Plan limits
# ═══════════════════════════════════════════════════════════════════════════════
# MODEL CATALOG — Single source of truth for all model metadata
# ═══════════════════════════════════════════════════════════════════════════════
# To add a new model:
# 1. Add it here with name/provider/badge/description
# 2. Add its model_id → provider mapping in MODEL_PROVIDERS
# 3. Add it to the appropriate plan(s) in PLAN_LIMITS
# That's it — the frontend loads everything from GET /api/v1/models/available
# ═══════════════════════════════════════════════════════════════════════════════
MODEL_CATALOG = {
# ── Free tier (Fireworks - lightweight) ────────────────────────────────────
"accounts/fireworks/models/kimi-k2-instruct-0905": {
"name": "Kimi K2",
"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",
"provider": "Fireworks AI",
"badge": "Fast",
"description": "Fast open-source model, great for most tasks",
},
"accounts/fireworks/models/mixtral-8x7b-instruct": {
"name": "Mixtral 8x7B",
"provider": "Fireworks AI",
"badge": "Balanced",
"description": "Balanced speed and quality",
},
"accounts/fireworks/models/qwen2p5-72b-instruct": {
"name": "Qwen 2.5 72B",
"provider": "Fireworks AI",
"badge": "Multilingual",
"description": "Excellent multilingual capabilities",
},
# ── Pro tier (Premium providers) ───────────────────────────────────────────
"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",
"provider": "OpenAI",
"badge": "Efficient",
"description": "Cost-effective for simpler tasks",
},
"claude-3-5-sonnet-20241022": {
"name": "Claude 3.5 Sonnet",
"provider": "Anthropic",
"badge": "Reasoning",
"description": "Excellent at analysis and reasoning",
},
"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",
"provider": "Google",
"badge": "Long Context",
"description": "Handles very long documents well",
},
}
# ─── Model ID → LLM provider mapping (used by llm_client.py for routing) ─────
MODEL_PROVIDERS = {
"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",
"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",
}
# ─── 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",
"pro": "gpt-4o",
"enterprise": "gpt-4o",
}
# ─── Plan limits ──────────────────────────────────────────────────────────────
PLAN_LIMITS = { PLAN_LIMITS = {
"free": { "free": {
"max_chatbots": 999999, # unlimited creation "max_chatbots": 999999, # unlimited creation
"max_published": 0, # cannot publish "max_published": 0, # cannot publish
"models": [], "models": [
"accounts/fireworks/models/kimi-k2-instruct-0905",
],
"conversations_limit": 999999, # unlimited preview "conversations_limit": 999999, # unlimited preview
"code_export": False, "code_export": False,
"features": ["preview_mode", "testing"], "features": ["preview_mode", "testing"],
@@ -67,8 +177,9 @@ PLAN_LIMITS = {
"max_published": 1, "max_published": 1,
"models": [ "models": [
"accounts/fireworks/models/kimi-k2-instruct-0905", "accounts/fireworks/models/kimi-k2-instruct-0905",
"accounts/fireworks/models/deepseek-v3p2", "accounts/fireworks/models/llama-v3p1-70b-instruct",
"accounts/fireworks/models/glm-4p7", "accounts/fireworks/models/mixtral-8x7b-instruct",
"accounts/fireworks/models/qwen2p5-72b-instruct",
], ],
"conversations_limit": 5000, "conversations_limit": 5000,
"code_export": False, "code_export": False,
@@ -79,7 +190,8 @@ PLAN_LIMITS = {
"max_published": 3, "max_published": 3,
"models": [ "models": [
"accounts/fireworks/models/kimi-k2-instruct-0905", "accounts/fireworks/models/kimi-k2-instruct-0905",
"accounts/fireworks/models/deepseek-v3p2", "accounts/fireworks/models/llama-v3p1-70b-instruct",
"accounts/fireworks/models/mixtral-8x7b-instruct",
"gpt-4o", "gpt-4o",
"gpt-4-turbo", "gpt-4-turbo",
"gpt-3.5-turbo", "gpt-3.5-turbo",
@@ -101,27 +213,9 @@ PLAN_LIMITS = {
"enterprise": { "enterprise": {
"max_chatbots": 999999, "max_chatbots": 999999,
"max_published": 999999, "max_published": 999999,
"models": ["*"], "models": ["*"], # resolves to all MODEL_CATALOG keys
"conversations_limit": 999999, "conversations_limit": 999999,
"code_export": True, "code_export": True,
"features": ["*"], "features": ["*"],
}, },
} }
MODEL_PROVIDERS = {
"accounts/fireworks/models/kimi-k2-instruct-0905": "fireworks",
"accounts/fireworks/models/deepseek-v3p2": "fireworks",
"accounts/fireworks/models/glm-4p7": "fireworks",
"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",
}
DEFAULT_MODELS = {
"starter": "accounts/fireworks/models/kimi-k2-instruct-0905",
"pro": "gpt-4o",
"enterprise": "gpt-4o",
}

View File

@@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse
import logging import logging
from app.config import settings from app.config import settings
from app.routers import auth, chatbots, documents, chat, marketplace, billing from app.routers import auth, chatbots, documents, chat, marketplace, billing, models
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@@ -53,6 +53,7 @@ app.include_router(documents.router, prefix="/api/v1")
app.include_router(chat.router, prefix="/api/v1") app.include_router(chat.router, prefix="/api/v1")
app.include_router(marketplace.router, prefix="/api/v1") app.include_router(marketplace.router, prefix="/api/v1")
app.include_router(billing.router, prefix="/api/v1") app.include_router(billing.router, prefix="/api/v1")
app.include_router(models.router, prefix="/api/v1")
# ── Health & Info ────────────────────────────────────────────────────────────── # ── Health & Info ──────────────────────────────────────────────────────────────
@app.get("/") @app.get("/")

View File

@@ -103,6 +103,7 @@ class ChatbotCreate(BaseModel):
max_tokens: int = Field(default=1000, ge=100, le=8000) max_tokens: int = Field(default=1000, ge=100, le=8000)
primary_color: str = "#6366f1" primary_color: str = "#6366f1"
welcome_message: str = "Hello! How can I help you today?" welcome_message: str = "Hello! How can I help you today?"
logo_url: Optional[str] = None
category: Optional[str] = None category: Optional[str] = None
industry: Optional[str] = None industry: Optional[str] = None
languages: List[str] = ["en"] languages: List[str] = ["en"]
@@ -117,6 +118,7 @@ class ChatbotUpdate(BaseModel):
max_tokens: Optional[int] = None max_tokens: Optional[int] = None
primary_color: Optional[str] = None primary_color: Optional[str] = None
welcome_message: Optional[str] = None welcome_message: Optional[str] = None
logo_url: Optional[str] = None
category: Optional[str] = None category: Optional[str] = None
industry: Optional[str] = None industry: Optional[str] = None
languages: Optional[List[str]] = None languages: Optional[List[str]] = None
@@ -133,6 +135,7 @@ class ChatbotResponse(BaseModel):
max_tokens: int max_tokens: int
primary_color: str primary_color: str
welcome_message: str welcome_message: str
logo_url: Optional[str] = None
category: Optional[str] = None category: Optional[str] = None
industry: Optional[str] = None industry: Optional[str] = None
languages: List[str] languages: List[str]
@@ -156,6 +159,7 @@ class ChatbotPublicResponse(BaseModel):
languages: List[str] languages: List[str]
primary_color: str primary_color: str
welcome_message: str welcome_message: str
logo_url: Optional[str] = None
average_rating: Optional[float] = None average_rating: Optional[float] = None
total_conversations: int = 0 total_conversations: int = 0
company_name: Optional[str] = None company_name: Optional[str] = None
@@ -303,4 +307,4 @@ class SuccessResponse(BaseModel):
class ErrorResponse(BaseModel): class ErrorResponse(BaseModel):
error: str error: str
detail: Optional[str] = None detail: Optional[str] = None

View File

@@ -53,7 +53,6 @@ async def create_chatbot(data: ChatbotCreate, user=Depends(get_current_user)):
vector_store.create_collection(collection_name) vector_store.create_collection(collection_name)
except Exception as e: except Exception as e:
logger.error(f"Failed to create Qdrant collection: {e}") logger.error(f"Failed to create Qdrant collection: {e}")
# Continue without vector store for now
collection_name = None collection_name = None
chatbot_data = { chatbot_data = {
@@ -67,6 +66,7 @@ async def create_chatbot(data: ChatbotCreate, user=Depends(get_current_user)):
"max_tokens": data.max_tokens, "max_tokens": data.max_tokens,
"primary_color": data.primary_color, "primary_color": data.primary_color,
"welcome_message": data.welcome_message, "welcome_message": data.welcome_message,
"logo_url": data.logo_url,
"category": data.category, "category": data.category,
"industry": data.industry, "industry": data.industry,
"languages": data.languages, "languages": data.languages,
@@ -227,6 +227,7 @@ def _format_chatbot(chatbot: dict, supabase) -> ChatbotResponse:
max_tokens=chatbot.get("max_tokens", 1000), max_tokens=chatbot.get("max_tokens", 1000),
primary_color=chatbot.get("primary_color", "#6366f1"), primary_color=chatbot.get("primary_color", "#6366f1"),
welcome_message=chatbot.get("welcome_message", "Hello! How can I help?"), welcome_message=chatbot.get("welcome_message", "Hello! How can I help?"),
logo_url=chatbot.get("logo_url"),
category=chatbot.get("category"), category=chatbot.get("category"),
industry=chatbot.get("industry"), industry=chatbot.get("industry"),
languages=chatbot.get("languages", ["en"]), languages=chatbot.get("languages", ["en"]),
@@ -237,4 +238,4 @@ def _format_chatbot(chatbot: dict, supabase) -> ChatbotResponse:
conversation_count=conv_count.count or 0, conversation_count=conv_count.count or 0,
created_at=chatbot.get("created_at"), created_at=chatbot.get("created_at"),
published_at=chatbot.get("published_at"), published_at=chatbot.get("published_at"),
) )

View File

@@ -60,6 +60,7 @@ async def list_marketplace_chatbots(
languages=c.get("languages", ["en"]), languages=c.get("languages", ["en"]),
primary_color=c.get("primary_color", "#6366f1"), primary_color=c.get("primary_color", "#6366f1"),
welcome_message=c.get("welcome_message", "Hello!"), welcome_message=c.get("welcome_message", "Hello!"),
logo_url=c.get("logo_url"),
average_rating=c.get("average_rating"), average_rating=c.get("average_rating"),
total_conversations=c.get("total_conversations", 0), total_conversations=c.get("total_conversations", 0),
company_name=company_data.get("name"), company_name=company_data.get("name"),
@@ -74,23 +75,27 @@ async def list_marketplace_chatbots(
total=total, total=total,
page=page, page=page,
limit=limit, limit=limit,
has_more=(offset + limit) < total, has_more=(offset + limit < total),
) )
@router.get("/chatbots/{chatbot_id}", response_model=ChatbotPublicResponse) @router.get("/chatbots/{chatbot_id}", response_model=ChatbotPublicResponse)
async def get_marketplace_chatbot(chatbot_id: str): async def get_marketplace_chatbot(chatbot_id: str, user=Depends(get_optional_user)):
supabase = get_supabase() supabase = get_supabase()
result = supabase.table("chatbots").select("*, companies(name, logo_url)") \
.eq("id", chatbot_id) \ result = supabase.table("chatbots").select(
.eq("is_published", True) \ "*, companies(name, logo_url)"
.execute() ).eq("id", chatbot_id).eq("is_published", True).execute()
if not result.data: if not result.data:
raise HTTPException(status_code=404, detail="Chatbot not found in marketplace") raise HTTPException(status_code=404, detail="Chatbot not found")
c = result.data[0] c = result.data[0]
company_data = c.get("companies") or {} company_data = c.get("companies") or {}
conv_count = supabase.table("conversations").select("id", count="exact") \
.eq("chatbot_id", chatbot_id).execute()
return ChatbotPublicResponse( return ChatbotPublicResponse(
id=c["id"], id=c["id"],
name=c["name"], name=c["name"],
@@ -100,8 +105,9 @@ async def get_marketplace_chatbot(chatbot_id: str):
languages=c.get("languages", ["en"]), languages=c.get("languages", ["en"]),
primary_color=c.get("primary_color", "#6366f1"), primary_color=c.get("primary_color", "#6366f1"),
welcome_message=c.get("welcome_message", "Hello!"), welcome_message=c.get("welcome_message", "Hello!"),
logo_url=c.get("logo_url"),
average_rating=c.get("average_rating"), average_rating=c.get("average_rating"),
total_conversations=c.get("total_conversations", 0), total_conversations=conv_count.count or 0,
company_name=company_data.get("name"), company_name=company_data.get("name"),
company_logo=company_data.get("logo_url"), company_logo=company_data.get("logo_url"),
created_at=c.get("created_at"), created_at=c.get("created_at"),
@@ -130,4 +136,4 @@ async def rate_chatbot(
new_avg = (current + rating.rating) / 2 new_avg = (current + rating.rating) / 2
supabase.table("chatbots").update({"average_rating": round(new_avg, 1)}).eq("id", chatbot_id).execute() 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)} return {"message": "Rating submitted", "new_average": round(new_avg, 1)}

107
app/routers/models.py Normal file
View File

@@ -0,0 +1,107 @@
"""
Models router - serves available AI models based on user subscription plan.
Single source of truth flow:
config.py (PLAN_LIMITS + MODEL_CATALOG) → this router → frontend
To add/remove/rename a model, only edit config.py.
"""
from fastapi import APIRouter, Depends
from app.dependencies import get_current_user
from app.database import get_supabase
from app.config import PLAN_LIMITS, MODEL_CATALOG, DEFAULT_MODELS
from typing import List, Optional
from pydantic import BaseModel
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/models", tags=["Models"])
# ─── Response Models ───────────────────────────────────────────────────────────
class AvailableModel(BaseModel):
id: str
name: str
provider: str
badge: str
description: Optional[str] = None
is_default: bool = False
class ModelsResponse(BaseModel):
models: List[AvailableModel]
plan: str
default_model: Optional[str] = None
has_premium_access: bool
upgrade_label: Optional[str] = None
# ─── Helpers ───────────────────────────────────────────────────────────────────
def _get_user_plan(user_id: str) -> str:
"""Get user's current subscription plan."""
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"
# ─── Endpoint ─────────────────────────────────────────────────────────────────
@router.get("/available", response_model=ModelsResponse)
async def get_available_models(user=Depends(get_current_user)):
"""
Returns the list of AI models the user can access based on their plan.
Frontend uses this to populate model selection dynamically.
- free: gets a default model for preview/testing
- starter: Fireworks AI models
- pro: Fireworks + OpenAI + Anthropic + Google
- enterprise: all models
"""
plan = _get_user_plan(user.id)
plan_config = PLAN_LIMITS.get(plan, PLAN_LIMITS["free"])
allowed_model_ids = plan_config.get("models", [])
# Enterprise wildcard → resolve to all catalog models
if "*" in allowed_model_ids:
allowed_model_ids = list(MODEL_CATALOG.keys())
# Build model list from catalog metadata
default_model_id = DEFAULT_MODELS.get(plan)
models: List[AvailableModel] = []
for model_id in allowed_model_ids:
meta = MODEL_CATALOG.get(model_id)
if not meta:
logger.warning(f"Model '{model_id}' in PLAN_LIMITS[{plan}] but not in MODEL_CATALOG — skipping")
continue
models.append(AvailableModel(
id=model_id,
name=meta["name"],
provider=meta["provider"],
badge=meta["badge"],
description=meta.get("description"),
is_default=(model_id == default_model_id),
))
# Determine upgrade messaging
has_premium = plan in ("pro", "enterprise")
upgrade_label = None
if plan == "free":
upgrade_label = "Upgrade to Starter for more models and publishing"
elif plan == "starter":
upgrade_label = "Upgrade to Pro for GPT-4o, Claude, Gemini"
return ModelsResponse(
models=models,
plan=plan,
default_model=default_model_id,
has_premium_access=has_premium,
upgrade_label=upgrade_label,
)