# -*- coding: utf-8 -*- """ llm_api.py – v1.2.0 (zentraler .env-Bootstrap, saubere Router-Einbindung, Swagger-Doku) Änderungen ggü. v1.1.6: - Zentrales .env-Bootstrapping VOR allen Router-Imports (findet Datei robust; setzt LLMAPI_ENV_FILE/LLMAPI_ENV_BOOTSTRAPPED) - Konsistente Swagger-Beschreibung + Tags-Metadaten - Router ohne doppelte Prefixe einbinden (die Prefixe werden in den Routern definiert) - Root-/health und /version Endpoints - Defensive Includes (Router-Importfehler verhindern Server-Absturz; Logging statt Crash) - Beibehaltener globaler Fehlerhandler (generische 500) Hinweis: - wiki_router im Canvas (v1.4.2) nutzt bereits robustes .env-Loading, respektiert aber die zentral gesetzten ENV-Variablen. - Wenn du ENV-Datei an anderem Ort hast, setze in der Systemd-Unit `Environment=LLMAPI_ENV_FILE=/pfad/.env`. """ from __future__ import annotations import os from pathlib import Path from textwrap import dedent from typing import Optional from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware # ---------------------- # Zentraler .env-Bootstrap (VOR Router-Imports ausführen!) # ---------------------- def _bootstrap_env() -> Optional[str]: try: from dotenv import load_dotenv, find_dotenv except Exception: print("[env] python-dotenv nicht installiert – überspringe .env-Loading", flush=True) return None candidates: list[str] = [] if os.getenv("LLMAPI_ENV_FILE"): candidates.append(os.getenv("LLMAPI_ENV_FILE") or "") fd = find_dotenv(".env", usecwd=True) if fd: candidates.append(fd) candidates += [ str(Path.cwd() / ".env"), str(Path(__file__).parent / ".env"), str(Path.home() / ".env"), str(Path.home() / ".llm-api.env"), "/etc/llm-api.env", ] for p in candidates: try: if p and Path(p).exists(): if load_dotenv(p, override=False): os.environ["LLMAPI_ENV_FILE"] = p os.environ["LLMAPI_ENV_BOOTSTRAPPED"] = "1" print(f"[env] loaded: {p}", flush=True) return p except Exception as e: print(f"[env] load failed for {p}: {e}", flush=True) print("[env] no .env found; using process env", flush=True) return None _ENV_SRC = _bootstrap_env() # ---------------------- # App + OpenAPI-Metadaten # ---------------------- __version__ = "1.2.0" print(f"[DEBUG] llm_api.py version {__version__} loaded from {__file__}", flush=True) TAGS = [ { "name": "wiki", "description": dedent( """ MediaWiki-Proxy (Health, Login, Page-Info/Parse, SMW-Ask). **ENV**: `WIKI_API_URL`, `WIKI_TIMEOUT`, `WIKI_RETRIES`, `WIKI_SLEEP_MS`, `WIKI_BATCH`. """ ), }, { "name": "exercise", "description": dedent( """ Übungen (Upsert, Suche, Delete). Upsert-Schlüssel: `external_id` (z. B. `mw:{pageid}`). **ENV**: `EXERCISE_COLLECTION`, `QDRANT_HOST`, `QDRANT_PORT`. """ ), }, { "name": "plans", "description": "Trainingspläne (Templates/Generate/Export).", }, ] app = FastAPI( title="KI Trainerassistent API", description=dedent( f""" Modulare API für Trainingsplanung und MediaWiki-Import. **Version:** {__version__} ## Quickstart (CLI) ```bash python3 wiki_importer.py --all python3 wiki_importer.py --all --category "Übungen" --dry-run ``` """ ), version=__version__, openapi_tags=TAGS, swagger_ui_parameters={"docExpansion": "list", "defaultModelsExpandDepth": 0}, ) # Optional: CORS für lokale UIs/Tools app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # ---------------------- # Globaler Fehlerhandler (generisch) # ---------------------- @app.exception_handler(Exception) async def unicorn_exception_handler(request, exc): return JSONResponse(status_code=500, content={"detail": "Interner Serverfehler."}) # ---------------------- # Router einbinden (WICHTIG: keine zusätzlichen Prefixe hier setzen) # ---------------------- def _include_router_safely(name: str, import_path: str): try: module = __import__(import_path, fromlist=["router"]) # lazy import nach ENV-Bootstrap app.include_router(module.router) print(f"[router] {name} included", flush=True) except Exception as e: print(f"[router] {name} NOT included: {e}", flush=True) _include_router_safely("wiki_router", "wiki_router") # prefix in Datei: /import/wiki _include_router_safely("embed_router", "embed_router") _include_router_safely("exercise_router", "exercise_router") _include_router_safely("plan_router", "plan_router") _include_router_safely("plan_session_router", "plan_session_router") # ---------------------- # Basis-Endpunkte # ---------------------- @app.get("/health", tags=["wiki"], summary="API-Health (lokal)") def api_health(): return {"status": "ok"} @app.get("/version", tags=["wiki"], summary="API-Version & ENV-Quelle") def api_version(): return {"version": __version__, "env_file": _ENV_SRC}