Files
ALwrity/backend/app.py

481 lines
16 KiB
Python

"""Main FastAPI application for ALwrity backend."""
from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel
from typing import Dict, Any, Optional
import os
import time
from collections import defaultdict
from loguru import logger
from dotenv import load_dotenv
import asyncio
from middleware.monitoring_middleware import monitoring_middleware
# Load environment variables
load_dotenv()
# Import the new enhanced functions
from api.onboarding import (
health_check,
get_onboarding_status,
get_onboarding_progress_full,
get_step_data,
complete_step,
skip_step,
validate_step_access,
get_api_keys,
save_api_key,
validate_api_keys,
start_onboarding,
complete_onboarding,
reset_onboarding,
get_resume_info,
get_onboarding_config,
get_provider_setup_info,
get_all_providers_info,
validate_provider_key,
get_enhanced_validation_status,
get_onboarding_summary,
get_website_analysis_data,
get_research_preferences_data,
StepCompletionRequest,
APIKeyRequest
)
# Import component logic endpoints
from api.component_logic import router as component_logic_router
# Import SEO tools router
from routers.seo_tools import router as seo_tools_router
# Import user data endpoints
# Import content planning endpoints
from api.content_planning.api.router import router as content_planning_router
from api.user_data import router as user_data_router
# Import database service
from services.database import init_database, close_database
# Import SEO Dashboard endpoints
from api.seo_dashboard import (
get_seo_dashboard_data,
get_seo_health_score,
get_seo_metrics,
get_platform_status,
get_ai_insights,
seo_dashboard_health_check,
analyze_seo_comprehensive,
analyze_seo_full,
get_seo_metrics_detailed,
get_analysis_summary,
batch_analyze_urls,
SEOAnalysisRequest
)
# Initialize FastAPI app
app = FastAPI(
title="ALwrity Backend API",
description="Backend API for ALwrity - AI-powered content creation platform",
version="2.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # React dev server
"http://localhost:8000", # Backend dev server
"http://localhost:3001", # Alternative React port
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Add API monitoring middleware
app.middleware("http")(monitoring_middleware)
# Simple rate limiting
request_counts = defaultdict(list)
RATE_LIMIT_WINDOW = 60 # 60 seconds
RATE_LIMIT_MAX_REQUESTS = 200 # Increased for testing - calendar generation polling
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
"""Simple rate limiting middleware with exemptions for streaming endpoints."""
try:
client_ip = request.client.host if request.client else "unknown"
current_time = time.time()
# Exempt streaming endpoints and frequently called endpoints from rate limiting
path = request.url.path
if any(streaming_path in path for streaming_path in [
"/stream/strategies",
"/stream/strategic-intelligence",
"/stream/keyword-research",
"/latest-strategy", # Exempt latest strategy endpoint from rate limiting
"/ai-analytics", # Exempt AI analytics endpoint from rate limiting
"/gap-analysis", # Exempt gap analysis endpoint from rate limiting
"/calendar-events", # Exempt calendar events endpoint from rate limiting
"/calendar-generation/progress", # Exempt calendar generation progress from rate limiting
"/health" # Exempt health check endpoints from rate limiting
]):
# Allow streaming endpoints without rate limiting
response = await call_next(request)
return response
# Clean old requests
request_counts[client_ip] = [req_time for req_time in request_counts[client_ip]
if current_time - req_time < RATE_LIMIT_WINDOW]
# Check rate limit
if len(request_counts[client_ip]) >= RATE_LIMIT_MAX_REQUESTS:
logger.warning(f"Rate limit exceeded for {client_ip}")
return JSONResponse(
status_code=429,
content={"detail": "Too many requests", "retry_after": RATE_LIMIT_WINDOW},
headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*"
}
)
# Add current request
request_counts[client_ip].append(current_time)
response = await call_next(request)
return response
except Exception as e:
logger.error(f"Error in rate limiting middleware: {e}")
# Continue without rate limiting if there's an error
response = await call_next(request)
return response
# Health check endpoint
@app.get("/health")
async def health():
"""Health check endpoint."""
return health_check()
# Onboarding status endpoints
@app.get("/api/onboarding/status")
async def onboarding_status():
"""Get the current onboarding status."""
try:
return await get_onboarding_status()
except Exception as e:
logger.error(f"Error in onboarding_status: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/onboarding/progress")
async def onboarding_progress():
"""Get the full onboarding progress data."""
try:
return await get_onboarding_progress_full()
except Exception as e:
logger.error(f"Error in onboarding_progress: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Step management endpoints
@app.get("/api/onboarding/step/{step_number}")
async def step_data(step_number: int):
"""Get data for a specific step."""
try:
return await get_step_data(step_number)
except Exception as e:
logger.error(f"Error in step_data: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/step/{step_number}/complete")
async def step_complete(step_number: int, request: StepCompletionRequest):
"""Mark a step as completed."""
try:
return await complete_step(step_number, request)
except Exception as e:
logger.error(f"Error in step_complete: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/step/{step_number}/skip")
async def step_skip(step_number: int):
"""Skip a step (for optional steps)."""
try:
return await skip_step(step_number)
except Exception as e:
logger.error(f"Error in step_skip: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/onboarding/step/{step_number}/validate")
async def step_validate(step_number: int):
"""Validate if user can access a specific step."""
try:
return await validate_step_access(step_number)
except Exception as e:
logger.error(f"Error in step_validate: {e}")
raise HTTPException(status_code=500, detail=str(e))
# API key management endpoints
@app.get("/api/onboarding/api-keys")
async def api_keys():
"""Get all configured API keys (masked)."""
try:
return await get_api_keys()
except Exception as e:
logger.error(f"Error in api_keys: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/api-keys")
async def api_key_save(request: APIKeyRequest):
"""Save an API key for a provider."""
try:
return await save_api_key(request)
except Exception as e:
logger.error(f"Error in api_key_save: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/api-keys/validate")
async def api_key_validate():
"""Validate all configured API keys."""
try:
return await validate_api_keys()
except Exception as e:
logger.error(f"Error in api_key_validate: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Onboarding control endpoints
@app.post("/api/onboarding/start")
async def onboarding_start():
"""Start a new onboarding session."""
try:
return await start_onboarding()
except Exception as e:
logger.error(f"Error in onboarding_start: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/complete")
async def onboarding_complete():
"""Complete the onboarding process."""
try:
return await complete_onboarding()
except Exception as e:
logger.error(f"Error in onboarding_complete: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/reset")
async def onboarding_reset():
"""Reset the onboarding progress."""
try:
return await reset_onboarding()
except Exception as e:
logger.error(f"Error in onboarding_reset: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Resume functionality
@app.get("/api/onboarding/resume")
async def onboarding_resume():
"""Get information for resuming onboarding."""
try:
return await get_resume_info()
except Exception as e:
logger.error(f"Error in onboarding_resume: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Configuration endpoints
@app.get("/api/onboarding/config")
async def onboarding_config():
"""Get onboarding configuration and requirements."""
try:
return get_onboarding_config()
except Exception as e:
logger.error(f"Error in onboarding_config: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Enhanced provider endpoints
@app.get("/api/onboarding/providers/{provider}/setup")
async def provider_setup_info(provider: str):
"""Get setup information for a specific provider."""
try:
return await get_provider_setup_info(provider)
except Exception as e:
logger.error(f"Error in provider_setup_info: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/onboarding/providers")
async def all_providers_info():
"""Get setup information for all providers."""
try:
return await get_all_providers_info()
except Exception as e:
logger.error(f"Error in all_providers_info: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/onboarding/providers/{provider}/validate")
async def validate_provider_key_endpoint(provider: str, request: APIKeyRequest):
"""Validate a specific provider's API key."""
try:
return await validate_provider_key(provider, request)
except Exception as e:
logger.error(f"Error in validate_provider_key: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/onboarding/validation/enhanced")
async def enhanced_validation_status():
"""Get enhanced validation status for all configured services."""
try:
return await get_enhanced_validation_status()
except Exception as e:
logger.error(f"Error in enhanced_validation_status: {e}")
raise HTTPException(status_code=500, detail=str(e))
# New endpoints for FinalStep data loading
@app.get("/api/onboarding/summary")
async def onboarding_summary():
"""Get comprehensive onboarding summary for FinalStep."""
try:
return await get_onboarding_summary()
except Exception as e:
logger.error(f"Error in onboarding_summary: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/onboarding/website-analysis")
async def website_analysis_data():
"""Get website analysis data for FinalStep."""
try:
return await get_website_analysis_data()
except Exception as e:
logger.error(f"Error in website_analysis_data: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/onboarding/research-preferences")
async def research_preferences_data():
"""Get research preferences data for FinalStep."""
try:
return await get_research_preferences_data()
except Exception as e:
logger.error(f"Error in research_preferences_data: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Include component logic router
app.include_router(component_logic_router)
# Include SEO tools router
app.include_router(seo_tools_router)
# Include user data router
# Include content planning router
app.include_router(content_planning_router)
app.include_router(user_data_router)
# SEO Dashboard endpoints
@app.get("/api/seo-dashboard/data")
async def seo_dashboard_data():
"""Get complete SEO dashboard data."""
return await get_seo_dashboard_data()
@app.get("/api/seo-dashboard/health-score")
async def seo_health_score():
"""Get SEO health score."""
return await get_seo_health_score()
@app.get("/api/seo-dashboard/metrics")
async def seo_metrics():
"""Get SEO metrics."""
return await get_seo_metrics()
@app.get("/api/seo-dashboard/platforms")
async def seo_platforms():
"""Get platform status."""
return await get_platform_status()
@app.get("/api/seo-dashboard/insights")
async def seo_insights():
"""Get AI insights."""
return await get_ai_insights()
@app.get("/api/seo-dashboard/health")
async def seo_dashboard_health():
"""Health check for SEO dashboard."""
return await seo_dashboard_health_check()
# Comprehensive SEO Analysis endpoints
@app.post("/api/seo-dashboard/analyze-comprehensive")
async def analyze_seo_comprehensive_endpoint(request: SEOAnalysisRequest):
"""Analyze a URL for comprehensive SEO performance."""
return await analyze_seo_comprehensive(request)
@app.post("/api/seo-dashboard/analyze-full")
async def analyze_seo_full_endpoint(request: SEOAnalysisRequest):
"""Analyze a URL for comprehensive SEO performance."""
return await analyze_seo_full(request)
@app.get("/api/seo-dashboard/metrics-detailed")
async def seo_metrics_detailed(url: str):
"""Get detailed SEO metrics for a URL."""
return await get_seo_metrics_detailed(url)
@app.get("/api/seo-dashboard/analysis-summary")
async def seo_analysis_summary(url: str):
"""Get a quick summary of SEO analysis for a URL."""
return await get_analysis_summary(url)
@app.post("/api/seo-dashboard/batch-analyze")
async def batch_analyze_urls_endpoint(urls: list[str]):
"""Analyze multiple URLs in batch."""
return await batch_analyze_urls(urls)
# Serve React frontend (for production)
@app.get("/")
async def serve_frontend():
"""Serve the React frontend."""
# Check if frontend build exists
frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "build")
index_html = os.path.join(frontend_path, "index.html")
if os.path.exists(index_html):
return FileResponse(index_html)
else:
return {
"message": "Frontend not built. Please run 'npm run build' in the frontend directory.",
"api_docs": "/api/docs"
}
# Mount static files for React app (only if directory exists)
try:
frontend_build_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "build")
static_path = os.path.join(frontend_build_path, "static")
if os.path.exists(static_path):
app.mount("/static", StaticFiles(directory=static_path), name="static")
logger.info("Frontend static files mounted successfully")
else:
logger.info("Frontend build directory not found. Static files not mounted.")
except Exception as e:
logger.info(f"Could not mount static files: {e}")
# Startup event
@app.on_event("startup")
async def startup_event():
"""Initialize services on startup."""
try:
# Initialize database
init_database()
logger.info("ALwrity backend started successfully")
except Exception as e:
logger.error(f"Error during startup: {e}")
# Shutdown event
@app.on_event("shutdown")
async def shutdown_event():
"""Cleanup on shutdown."""
try:
# Close database connections
close_database()
logger.info("ALwrity backend shutdown successfully")
except Exception as e:
logger.error(f"Error during shutdown: {e}")