File size: 4,464 Bytes
38c016b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""
FastAPI backend for crossword puzzle generator with vector similarity search.
"""

import os
import logging
import time
from datetime import datetime
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import uvicorn
from dotenv import load_dotenv

from src.routes.api import router as api_router
from src.services.vector_search import VectorSearchService

# Load environment variables
load_dotenv()

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def log_with_timestamp(message):
    """Helper to log with precise timestamp."""
    timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
    logger.info(f"[{timestamp}] {message}")

# Global vector search service instance
vector_service = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Initialize and cleanup application resources."""
    global vector_service
    
    # Startup
    startup_time = time.time()
    log_with_timestamp("πŸš€ Initializing Python backend with vector search...")
    
    # Initialize vector search service
    try:
        service_start = time.time()
        log_with_timestamp("πŸ”§ Creating VectorSearchService instance...")
        vector_service = VectorSearchService()
        
        log_with_timestamp("⚑ Starting vector search initialization...")
        await vector_service.initialize()
        
        init_time = time.time() - service_start
        log_with_timestamp(f"βœ… Vector search service initialized in {init_time:.2f}s")
    except Exception as e:
        logger.error(f"❌ Failed to initialize vector search service: {e}")
        # Continue without vector search (will fallback to static words)
    
    # Make vector service available to routes
    app.state.vector_service = vector_service
    
    yield
    
    # Shutdown
    logger.info("πŸ›‘ Shutting down Python backend...")
    if vector_service:
        await vector_service.cleanup()

# Create FastAPI app
app = FastAPI(
    title="Crossword Puzzle Generator API",
    description="Python backend with AI-powered vector similarity search",
    version="2.0.0",
    lifespan=lifespan
)

# CORS configuration
cors_origins = []
if os.getenv("NODE_ENV") == "production":
    # Production: same origin
    cors_origins = ["*"]  # HuggingFace Spaces
else:
    # Development: allow dev servers
    cors_origins = [
        "http://localhost:5173",  # Vite dev server
        "http://localhost:3000",  # Alternative dev server
        "http://localhost:7860",  # Local production test
    ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=cors_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include API routes
app.include_router(api_router, prefix="/api")

# Serve static files (frontend)
static_path = Path(__file__).parent / "public"
if static_path.exists():
    app.mount("/assets", StaticFiles(directory=static_path / "assets"), name="assets")
    
    @app.get("/")
    async def serve_frontend():
        """Serve the React frontend."""
        index_path = static_path / "index.html"
        if index_path.exists():
            return FileResponse(index_path)
        else:
            raise HTTPException(status_code=404, detail="Frontend not found")
    
    @app.get("/{full_path:path}")
    async def serve_spa_routes(full_path: str):
        """Serve React SPA routes."""
        # For any non-API route, serve the React app
        if not full_path.startswith("api/"):
            index_path = static_path / "index.html"
            if index_path.exists():
                return FileResponse(index_path)
        raise HTTPException(status_code=404, detail="Not found")

@app.get("/health")
async def health_check():
    """Health check endpoint."""
    return {
        "status": "healthy",
        "backend": "python",
        "vector_search": vector_service.is_initialized if vector_service else False
    }

if __name__ == "__main__":
    port = int(os.getenv("PORT", 7860))
    host = "0.0.0.0" if os.getenv("NODE_ENV") == "production" else "127.0.0.1"
    
    logger.info(f"🐍 Starting Python backend on {host}:{port}")
    uvicorn.run(
        "app:app",
        host=host,
        port=port,
        reload=os.getenv("NODE_ENV") != "production"
    )