import asyncio import logging import os import sys from contextlib import asynccontextmanager from alembic import command from alembic.config import Config as AlembicConfig from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from config import settings # Настройка логов до импорта приложения: uvicorn ставит свои handlers # на root-logger, поэтому basicConfig в lifespan уже не срабатывает # (handlers есть — basicConfig no-op). force=True перебивает. logging.basicConfig( level=getattr(logging, settings.log_level.upper(), logging.INFO), format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", datefmt="%H:%M:%S", handlers=[logging.StreamHandler(sys.stderr)], force=True, ) from db.session import SessionLocal # noqa: E402 from services import config_service, intent_service, intent_step_graph_service, intent_step_service # noqa: E402 from services.embeddings import EmbeddingService # noqa: E402 from services.llm_client import LLMClient # noqa: E402 from services.router_client import RouterClient # noqa: E402 from services.vectorstore import VectorStoreService # noqa: E402 logger = logging.getLogger(__name__) embedding_service: EmbeddingService | None = None vectorstore_service: VectorStoreService | None = None llm_client: LLMClient | None = None router_client: RouterClient | None = None def _run_migrations() -> None: """Автоматически подтягиваем схему до последней ревизии при старте.""" os.makedirs(os.path.dirname(settings.sqlite_path), exist_ok=True) cfg = AlembicConfig("alembic.ini") command.upgrade(cfg, "head") @asynccontextmanager async def lifespan(app: FastAPI): global embedding_service, vectorstore_service, llm_client, router_client logger.info("Running DB migrations…") await asyncio.to_thread(_run_migrations) logger.info("Loading embedding model: %s", settings.embedding_model) embedding_service = EmbeddingService(settings.embedding_model) logger.info("Embedding model loaded") vectorstore_service = VectorStoreService( persist_dir=settings.chroma_persist_dir, embedding_service=embedding_service, ) logger.info("ChromaDB initialized at %s", settings.chroma_persist_dir) llm_client = LLMClient() router_client = RouterClient() logger.info("LLM + Router clients ready (model=%s)", llm_client.model) async with SessionLocal() as session: await intent_service.ensure_seed_intents(session) await intent_service.migrate_intent_copy(session) await config_service.migrate_legacy_config_to_general_info(session) await config_service.ensure_seed_configs(session) await config_service.migrate_exit_conditions_to_field(session) await intent_step_service.ensure_seed_steps(session) await intent_step_service.ensure_seed_guards(session) await intent_step_service.migrate_new_booking_allowed_next_v2(session) await intent_step_graph_service.ensure_seed_graphs(session) yield logger.info("Shutting down") app = FastAPI( title="Chat Agent for Patients — Tuning Tool", description="RAG-ядро и инструмент настройки пациентского чат-агента", version="0.1.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) from routers import chat, configs, documents, eval as eval_router, health, intents, query, threads # noqa: E402 app.include_router(health.router) app.include_router(documents.router) app.include_router(query.router) app.include_router(chat.router) app.include_router(threads.router) app.include_router(configs.router) app.include_router(intents.router) app.include_router(eval_router.router) @app.get("/api/docs/examples/{name}") def get_example_markdown(name: str): safe = "".join(c for c in name if c.isalnum() or c in "_-") if safe != name: raise HTTPException(status_code=400, detail="invalid example name") path = os.path.join("docs", "examples", f"{safe}.md") if not os.path.isfile(path): raise HTTPException(status_code=404, detail="example not found") return FileResponse(path, media_type="text/markdown; charset=utf-8") app.mount("/", StaticFiles(directory="static", html=True), name="static")