Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
172 lines
5.0 KiB
Python
172 lines
5.0 KiB
Python
"""
|
|
API endpoints for LLM-powered naming features.
|
|
Used by Trends and Forge tabs in the Hunt page.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.database import get_db
|
|
from app.models.subscription import Subscription, SubscriptionTier
|
|
from app.models.user import User
|
|
from app.services.llm_naming import (
|
|
expand_trend_keywords,
|
|
analyze_trend,
|
|
generate_brandable_names,
|
|
generate_similar_names,
|
|
)
|
|
|
|
|
|
router = APIRouter(prefix="/naming", tags=["LLM Naming"])
|
|
|
|
|
|
def _tier_level(tier: str) -> int:
|
|
t = (tier or "").lower()
|
|
if t == "tycoon":
|
|
return 3
|
|
if t == "trader":
|
|
return 2
|
|
return 1
|
|
|
|
|
|
async def _get_user_tier(db: AsyncSession, user: User) -> str:
|
|
res = await db.execute(select(Subscription).where(Subscription.user_id == user.id))
|
|
sub = res.scalar_one_or_none()
|
|
if not sub:
|
|
return "scout"
|
|
return sub.tier.value
|
|
|
|
|
|
async def _require_trader_or_above(db: AsyncSession, user: User):
|
|
"""Check that user has at least Trader tier."""
|
|
tier = await _get_user_tier(db, user)
|
|
if _tier_level(tier) < 2:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="AI naming features require Trader or Tycoon plan."
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# TRENDS TAB ENDPOINTS
|
|
# ============================================================================
|
|
|
|
class TrendExpandRequest(BaseModel):
|
|
trend: str = Field(..., min_length=1, max_length=100)
|
|
geo: str = Field(default="US", max_length=5)
|
|
|
|
|
|
class TrendExpandResponse(BaseModel):
|
|
keywords: list[str]
|
|
trend: str
|
|
|
|
|
|
@router.post("/trends/expand", response_model=TrendExpandResponse)
|
|
async def expand_trend(
|
|
request: TrendExpandRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
Expand a trending topic into related domain-friendly keywords.
|
|
Requires Trader or Tycoon subscription.
|
|
"""
|
|
await _require_trader_or_above(db, current_user)
|
|
|
|
keywords = await expand_trend_keywords(request.trend, request.geo)
|
|
return TrendExpandResponse(keywords=keywords, trend=request.trend)
|
|
|
|
|
|
class TrendAnalyzeRequest(BaseModel):
|
|
trend: str = Field(..., min_length=1, max_length=100)
|
|
geo: str = Field(default="US", max_length=5)
|
|
|
|
|
|
class TrendAnalyzeResponse(BaseModel):
|
|
analysis: str
|
|
trend: str
|
|
|
|
|
|
@router.post("/trends/analyze", response_model=TrendAnalyzeResponse)
|
|
async def analyze_trend_endpoint(
|
|
request: TrendAnalyzeRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
Get AI analysis of a trending topic for domain investors.
|
|
Requires Trader or Tycoon subscription.
|
|
"""
|
|
await _require_trader_or_above(db, current_user)
|
|
|
|
analysis = await analyze_trend(request.trend, request.geo)
|
|
return TrendAnalyzeResponse(analysis=analysis, trend=request.trend)
|
|
|
|
|
|
# ============================================================================
|
|
# FORGE TAB ENDPOINTS
|
|
# ============================================================================
|
|
|
|
class BrandableGenerateRequest(BaseModel):
|
|
concept: str = Field(..., min_length=3, max_length=200)
|
|
style: Optional[str] = Field(default=None, max_length=50)
|
|
count: int = Field(default=15, ge=5, le=30)
|
|
|
|
|
|
class BrandableGenerateResponse(BaseModel):
|
|
names: list[str]
|
|
concept: str
|
|
|
|
|
|
@router.post("/forge/generate", response_model=BrandableGenerateResponse)
|
|
async def generate_brandables(
|
|
request: BrandableGenerateRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
Generate brandable domain names based on a concept description.
|
|
Requires Trader or Tycoon subscription.
|
|
"""
|
|
await _require_trader_or_above(db, current_user)
|
|
|
|
names = await generate_brandable_names(
|
|
request.concept,
|
|
style=request.style,
|
|
count=request.count
|
|
)
|
|
return BrandableGenerateResponse(names=names, concept=request.concept)
|
|
|
|
|
|
class SimilarNamesRequest(BaseModel):
|
|
brand: str = Field(..., min_length=2, max_length=50)
|
|
count: int = Field(default=12, ge=5, le=20)
|
|
|
|
|
|
class SimilarNamesResponse(BaseModel):
|
|
names: list[str]
|
|
brand: str
|
|
|
|
|
|
@router.post("/forge/similar", response_model=SimilarNamesResponse)
|
|
async def generate_similar(
|
|
request: SimilarNamesRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
Generate names similar to an existing brand.
|
|
Requires Trader or Tycoon subscription.
|
|
"""
|
|
await _require_trader_or_above(db, current_user)
|
|
|
|
names = await generate_similar_names(request.brand, count=request.count)
|
|
return SimilarNamesResponse(names=names, brand=request.brand)
|
|
|