"""Memory Gateway — bootstrap FastAPI, lifecycle e middleware. Gli endpoint e la logica di dominio sono separati in moduli: config, models, state, audit, guardrail, embed, store, metrics, cleanup, routes. Il contratto HTTP resta invariato. """ from __future__ import annotations import asyncio import time import uuid from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI, Request from qdrant_client.http import models as qm import cleanup import embed as embedding import metrics import state from config import ( COLLECTION, EMBED_DIM, METRICS_ENABLED, SPARSE_VECTOR_NAME, GATEWAY_VERSION, log, ) from routes import router # Alias utili per compatibilità con import/debug locali; lo stato effettivo è in state.py. qdrant = state.qdrant embed = embedding.embed state.embed = embedding.embed state.sparse_encode = embedding.sparse_encode async def _lifespan(_app: FastAPI): """Crea collection/indici e avvia i loop periodici.""" collections = state.qdrant.get_collections().collections if not any(c.name == COLLECTION for c in collections): state.qdrant.create_collection( collection_name=COLLECTION, vectors_config=qm.VectorParams(size=EMBED_DIM, distance=qm.Distance.COSINE), sparse_vectors_config={SPARSE_VECTOR_NAME: qm.SparseVectorParams(modifier=qm.Modifier.IDF)}, ) for field in ("agent_id", "project_id", "scope", "kind", "supersedes_id", "superseded_by", "text_hash", "parent_id", "level", "topic"): state.qdrant.create_payload_index(collection_name=COLLECTION, field_name=field, field_schema=qm.PayloadSchemaType.KEYWORD) state.qdrant.create_payload_index(collection_name=COLLECTION, field_name="text", field_schema=qm.PayloadSchemaType.TEXT) log.info("collection %s creata con indici (dense + sparse %s)", COLLECTION, SPARSE_VECTOR_NAME) else: log.info("collection %s già esistente", COLLECTION) for field in ("parent_id", "level", "topic"): try: state.qdrant.create_payload_index(collection_name=COLLECTION, field_name=field, field_schema=qm.PayloadSchemaType.KEYWORD) except Exception: # noqa: BLE001 pass info = state.qdrant.get_collection(COLLECTION) sparse_vectors = (info.config.params.sparse_vectors or {}) if info.config and info.config.params else {} if SPARSE_VECTOR_NAME not in sparse_vectors: state.qdrant.create_vector_name(COLLECTION, SPARSE_VECTOR_NAME, qm.SparseVectorNameConfig(sparse=qm.SparseVectorConfig(modifier=qm.Modifier.IDF))) log.info("sparse vector %s aggiunto alla collection esistente", SPARSE_VECTOR_NAME) embedding.backfill_sparse(state.qdrant, COLLECTION) cleanup_task = asyncio.create_task(cleanup.loop(state.qdrant, COLLECTION, state.invalidate_meta)) metrics_task = asyncio.create_task(metrics.push_loop(state.qdrant, COLLECTION, embedding.get_http)) if METRICS_ENABLED else None try: yield finally: cleanup_task.cancel() try: await cleanup_task except asyncio.CancelledError: pass if metrics_task is not None: metrics_task.cancel() try: await metrics_task except asyncio.CancelledError: pass await embedding.close_http() app = FastAPI(title="Memory Gateway", version=GATEWAY_VERSION, lifespan=_lifespan) app.include_router(router) @app.middleware("http") async def request_id_middleware(request: Request, call_next): rid = request.headers.get("X-Request-ID") or str(uuid.uuid4()) state.request_id.set(rid) response = await call_next(request) response.headers["X-Request-ID"] = rid return response @app.middleware("http") async def metrics_middleware(request: Request, call_next): start = time.monotonic() response = await call_next(request) route = request.scope.get("route") endpoint = route.path if route else request.url.path metrics.record_request(endpoint, time.monotonic() - start, response.status_code) return response if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8080)