File size: 4,377 Bytes
266d7bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804054e
266d7bc
804054e
266d7bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
from contextlib import asynccontextmanager

import dotenv
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from qdrant_client.http.exceptions import UnexpectedResponse

from src.api.exceptions.exception_handlers import (
    general_exception_handler,
    qdrant_exception_handler,
    validation_exception_handler,
)
from src.api.middleware.logging_middleware import LoggingMiddleware
from src.api.routes.health_routes import router as health_router
from src.api.routes.search_routes import router as search_router
from src.infrastructure.qdrant.qdrant_vectorstore import AsyncQdrantVectorStore
from src.utils.logger_util import setup_logging

# Load environment variables from .env file
dotenv.load_dotenv()

# -----------------------
# Logger setup
# -----------------------
logger = setup_logging()


# -----------------------
# Lifespan
# -----------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
    """
    Lifespan context manager to handle startup and shutdown events.
    Initializes the Qdrant vector store on startup and ensures proper cleanup on shutdown.

    Args:
        app (FastAPI): The FastAPI application instance.
    Yields:
        None

    Exceptions:
        Raises exceptions if initialization or cleanup fails.
    """
    ## Ensure the cache directory exists and is writable (HF downloads the models here)
    cache_dir = "/tmp/fastembed_cache"
    os.makedirs(cache_dir, exist_ok=True)  # Ensure directory exists
    # Force /tmp/huggingface in Google Cloud so that it's writable.
    # This is the default cache dir of Huggingface.
    # Otherwise it tries ~/.cache/huggingface (read-only directory) in Google Cloud.
    # That directory is not writable.
    logger.info(f"HF_HOME: {os.environ.get('HF_HOME', 'Not set')}")
    logger.info(f"Cache dir: {cache_dir}, Writable: {os.access(cache_dir, os.W_OK)}")
    cache_contents = os.listdir(cache_dir) if os.path.exists(cache_dir) else "Empty"
    logger.info(f"Cache contents before: {cache_contents}")
    try:
        # creates Qdrant client internally
        app.state.vectorstore = AsyncQdrantVectorStore(cache_dir=cache_dir)
    except Exception as e:
        logger.exception("Failed to initialize QdrantVectorStore")
        raise e
    yield
    try:
        await app.state.vectorstore.client.close()
    except Exception:
        logger.exception("Failed to close Qdrant client")


# -----------------------
# FastAPI application
# -----------------------

app = FastAPI(
    title="Search Engine RAG API",
    version="1.0",
    description="API for Articles Search Retrieval-Augmented Generation (RAG) system",
    lifespan=lifespan,
    # root_path=root_path,
)


# -----------------------
# Middleware
# -----------------------


# Log the allowed origins
allowed_origins = os.getenv("ALLOWED_ORIGINS", "").split(",")
logger.info(f"CORS allowed origins: {allowed_origins}")

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,  # ["*"],  # allowed_origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "OPTIONS"],  # only the methods the app uses
    allow_headers=["Authorization", "Content-Type"],  # only headers needed
)

app.add_middleware(LoggingMiddleware)


# -----------------------
# Exception Handlers
# -----------------------
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(UnexpectedResponse, qdrant_exception_handler)
app.add_exception_handler(Exception, general_exception_handler)


# -----------------------
# Routers
# -----------------------
app.include_router(search_router, prefix="/search", tags=["search"])
app.include_router(health_router, tags=["health"])

# For Cloud Run, run the app directly
if __name__ == "__main__":
    import uvicorn

    port = int(os.environ.get("PORT", 8080))  # Cloud Run provides PORT env var

    uvicorn.run(
        "src.api.main:app",
        host="0.0.0.0",
        port=port,
        log_level="info",
        reload=True,  # Enable auto-reload for development
    )

    # config = uvicorn.Config(
    #     app,
    #     port=port,
    #     log_level="info",
    #     # loop="uvloop",
    #     # workers=1,
    #     reload=True
    #     )
    # server = uvicorn.Server(config)

    # server.run()