pounce/backend/app/api/llm_naming.py
Yves Gugger e135c3258b
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
Remove chat companion, add LLM naming for Trends & Forge tabs (AI keyword expansion, concept generator)
2025-12-17 15:45:16 +01:00

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)