File size: 6,891 Bytes
6c24b50
 
 
 
 
8fe1f14
6c24b50
 
 
 
08919be
89157f5
6c24b50
2ebf97a
 
3379d24
c2bb116
6c24b50
d02a73b
6c24b50
 
 
 
3379d24
c2bb116
3379d24
6c24b50
 
 
c25809b
6c24b50
 
 
 
 
 
 
 
 
 
c28ae12
6c24b50
 
 
 
08919be
 
 
 
c2bb116
 
 
 
4b54fab
3379d24
4b54fab
 
c2bb116
3379d24
2ebf97a
 
 
 
 
 
 
 
 
6c24b50
 
2ebf97a
 
c2bb116
89157f5
6c24b50
 
 
 
 
f7c1a9c
6c24b50
 
 
 
 
 
 
3379d24
ff6b176
c2bb116
f87115f
6c24b50
 
 
 
 
 
 
 
 
 
 
 
d02a73b
 
 
 
 
 
8ee0135
d02a73b
 
 
 
 
 
 
 
 
 
 
 
 
4789772
6c24b50
62ec94a
8604693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62ec94a
6c24b50
 
c2bb116
 
 
 
 
 
 
 
 
 
 
6c24b50
 
 
89157f5
6c24b50
 
 
 
 
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware

from app.config import get_settings
from app.core.auth.deps import init_auth_db
from app.core.database import pool_manager
from app.core.logger import get_logger
from app.core.redis_client import create_redis_client, close_redis
from app.core.scripts import load_scripts
from app.services.embeddings_service import EmbeddingService
from app.services.vector_store_service import VectorStoreService
from app.api.v1.router import api_v1_router
from app.api.v1.system import is_maintenance

_logger = get_logger(__name__)
_settings = get_settings()

_embedding_service: EmbeddingService = EmbeddingService()
_vector_store_service: VectorStoreService = VectorStoreService(_embedding_service)


async def _self_ping():
    import httpx
    health_url = _settings.self_ping_url
    while True:
        try:
            async with httpx.AsyncClient(timeout=30.0) as client:
                response = await client.get(health_url)
                if response.status_code == 200:
                    _logger.info("Self-ping successful: %s", health_url)
                else:
                    _logger.warning("Self-ping returned: %s - %s", health_url, response.status_code)
        except Exception as exc:
            _logger.error("Self-ping error: %s", exc)
        await asyncio.sleep(900)


@asynccontextmanager
async def lifespan(app: FastAPI):
    _logger.info("Initializing authentication database...")
    await init_auth_db()
    _logger.info("Authentication database initialized")

    _logger.info("Initializing vector store database...")
    await _vector_store_service.init_db()
    _logger.info("Vector store database initialized with %d stores", len(_vector_store_service.list_stores()))

    _logger.info("Initializing embedding service (loading 384-dim model)...")
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, _embedding_service.load_model, 384)
    _logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
    _logger.info("Vector store service initialized with %d existing stores", len(_vector_store_service.list_stores()))

    redis = create_redis_client(_settings.redis_url) if _settings.redis_url else None
    scripts = await load_scripts(redis) if redis else {}
    app.state.redis = redis
    app.state.scripts = scripts
    if redis:
        _logger.info("Redis and Lua scripts initialized")
    else:
        _logger.warning("Redis not configured, running in degraded mode")

    asyncio.create_task(_self_ping())
    yield
    _logger.info("Shutting down...")
    await close_redis(redis)
    await _vector_store_service.close_all()
    await pool_manager.close_all()


def create_application() -> FastAPI:
    app = FastAPI(
        title=_settings.app_name,
        description="All API Collection",
        version=_settings.app_version,
        docs_url="/docs",
        redoc_url="/redoc",
        openapi_tags=[
            {"name": "Convert", "description": "Single-file and single-URL conversion"},
            {"name": "Batch", "description": "Bulk conversion of files and URLs"},
            {"name": "System", "description": "Health, info, and supported formats"},
            {"name": "Embeddings", "description": "Text embedding generation using transformer models"},
            {"name": "Verify", "description": "Phone number and identity verification"},
            {"name": "Vector Stores", "description": "Create, manage, and search vector stores for RAG"},
            {"name": "URL Shortener", "description": "Create and manage short URLs with analytics"},
        ],
        lifespan=lifespan,
    )

    app.add_middleware(GZipMiddleware, minimum_size=1000)
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
    )

    @app.middleware("http")
    async def maintenance_middleware(request: Request, call_next):
        if is_maintenance():
            method = request.method
            path = request.url.path
            if method not in ("GET", "HEAD", "OPTIONS"):
                if not path.startswith("/api/v1/maintenance"):
                    if method == "POST" and path.startswith("/api/v1/backup"):
                        pass  # allow backup/restore during maintenance
                    else:
                        from starlette.responses import JSONResponse
                        return JSONResponse(
                            status_code=503,
                            content={
                                "success": False,
                                "detail": "Service is under maintenance. No write operations allowed.",
                            },
                        )
        return await call_next(request)

    app.include_router(api_v1_router, prefix="/api/v1")

    @app.get("/", include_in_schema=False)
    async def root(request: Request):
        from collections import defaultdict
        routes_by_tag: dict[str, list[dict]] = defaultdict(list)
        for route in app.routes:
            if not hasattr(route, "methods") or not hasattr(route, "path"):
                continue
            if route.path in ("/", "/health", "/ping", "/openapi.json", "/docs", "/redoc", "/docs/oauth2-redirect"):
                continue
            tags = getattr(route, "tags", None) or ["default"]
            for tag in tags:
                routes_by_tag[tag].append({
                    "method": list(route.methods - {"HEAD", "OPTIONS"}),
                    "path": route.path,
                    "summary": getattr(route, "summary", ""),
                })
        return {
            "name": _settings.app_name,
            "version": _settings.app_version,
            "docs": {
                "swagger": str(request.base_url) + "docs",
                "redoc": str(request.base_url) + "redoc",
            },
        }

    @app.get("/health", include_in_schema=False)
    async def root_health():
        store_count = len(_vector_store_service.list_stores())
        doc_count = await _vector_store_service.get_total_document_count()
        return {
            "success": True,
            "app_name": _settings.app_name,
            "version": _settings.app_version,
            "embedding_dimension": _settings.embedding_dimension,
            "vector_store_count": store_count,
            "total_documents": doc_count,
            "model_loaded": _embedding_service.is_loaded(384),
        }

    @app.get("/ping", include_in_schema=False)
    async def ping():
        return {"name": f"{_settings.app_name}", "version": _settings.app_version}

    return app


app = create_application()