matrix-ai / app /main.py
ruslanmv's picture
VectorDB
215df55
# app/main.py
from __future__ import annotations
import logging
import os
import time
from contextlib import asynccontextmanager
from typing import Any, Dict
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
# ---- Early env load (HF_TOKEN, ADMIN_TOKEN, GITHUB_TOKEN, etc.) ----
def _load_env_file(paths: list[str]) -> None:
"""
Load environment variables from the first existing path in `paths`.
Prefer python-dotenv if present; otherwise use a tiny fallback parser.
Does not override pre-existing env vars (e.g., Space Secrets).
"""
logger = logging.getLogger("uvicorn.error")
# 1) Try python-dotenv
try:
from dotenv import load_dotenv # type: ignore
for p in paths:
if os.path.exists(p):
load_dotenv(dotenv_path=p, override=False)
logger.info("Loaded environment from %s", p)
return
logger.info("No .env file found in %s (skipping)", paths)
return
except Exception:
# 2) Fallback minimal parser
for p in paths:
if not os.path.exists(p):
continue
try:
with open(p, "r", encoding="utf-8") as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[len("export ") :].strip()
if "=" not in line:
continue
key, val = line.split("=", 1)
key, val = key.strip(), val.strip()
# strip optional quotes
if (val.startswith('"') and val.endswith('"')) or (
val.startswith("'") and val.endswith("'")
):
val = val[1:-1]
# do not clobber existing env (e.g., HF Secrets)
os.environ.setdefault(key, val)
logger.info("Loaded environment from %s (fallback parser)", p)
return
except Exception as e:
logger.warning("Failed loading env from %s: %s", p, e)
logger.info("No .env loaded (none found / parsers failed)")
# Try common local locations. HF Spaces will rely on Secrets instead.
_load_env_file([".env", "configs/.env", ".env.local", "configs/.env.local"])
# ---- RAG bootstrap & warm-up ----
from .deps import get_settings
from .services.chat_service import get_retriever
from .core.rag.build import ensure_kb
# ---- Middlewares ----
try:
from .middleware import attach_middlewares # singular
except Exception:
try:
from .middlewares import attach_middlewares # plural
except Exception:
def attach_middlewares(app: FastAPI) -> None: # no-op fallback
logging.getLogger("uvicorn.error").warning(
"attach_middlewares not found; continuing without custom middlewares."
)
# ---- Routers ----
from .routers import health, plan, chat
# Optional UI bundle (/, /chat, /dev)
try:
from .ui import router as ui_router # type: ignore
HAS_UI = True
except Exception: # pragma: no cover
HAS_UI = False
TAGS_METADATA = [
{"name": "Health", "description": "Liveness / readiness probes and basic service metadata."},
{"name": "Planning", "description": "AI plan generation for Matrix Guardian (/v1/plan)."},
{"name": "Chat", "description": "Lightweight RAG/Q&A about Matrix System (/v1/chat)."},
{"name": "UI", "description": "Minimal web UI (Home, Chat, Dev) if enabled."},
]
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.started_at = time.time()
app.state.version = os.getenv("APP_VERSION", "1.0.0")
logger = logging.getLogger("uvicorn.error")
# 1) Build KB on first boot (skips if already present)
try:
if ensure_kb(
out_jsonl="data/kb.jsonl",
config_path="configs/rag_sources.yaml",
skip_if_exists=True,
):
logger.info("KB ready at data/kb.jsonl")
else:
logger.warning("KB build produced no records; running LLM-only.")
except Exception as e:
logger.warning("KB build failed (%s); running LLM-only.", e)
# 2) Warm up RAG retriever (indexes data/kb.jsonl if present)
logger.info("Warming up RAG retriever...")
get_retriever(get_settings())
logger.info("RAG retriever is ready.")
# 3) Boot log
hf_token_present = bool(os.getenv("HF_TOKEN"))
logger.info(
"matrix-ai starting (version=%s, port=%s, hf_token_present=%s)",
app.state.version,
os.getenv("PORT", "7860"),
"yes" if hf_token_present else "no",
)
try:
yield
finally:
uptime = time.time() - getattr(app.state, "started_at", time.time())
logger.info("matrix-ai shutting down (uptime=%.2fs)", uptime)
def create_app() -> FastAPI:
app = FastAPI(
title="matrix-ai",
version=os.getenv("APP_VERSION", "1.0.0"),
description="AI planning microservice for the Matrix EcoSystem",
openapi_tags=TAGS_METADATA,
docs_url="/docs",
redoc_url=None,
lifespan=lifespan,
)
# Middlewares (gzip, CORS, rate-limit, req-logs, etc.)
attach_middlewares(app)
# Core routers
app.include_router(health.router, tags=["Health"])
app.include_router(plan.router, prefix="/v1", tags=["Planning"])
app.include_router(chat.router, prefix="/v1", tags=["Chat"])
# UI (/, /chat, /dev). Your ui.py already defines "/" → /chat
if HAS_UI:
app.include_router(ui_router, tags=["UI"])
else:
# Minimal root so HF root probes pass even without UI
@app.get("/", include_in_schema=False)
async def root() -> Dict[str, Any]:
return {
"ok": True,
"service": "matrix-ai",
"version": app.version,
"docs": "/docs",
"endpoints": {"plan": "/v1/plan", "chat": "/v1/chat", "healthz": "/healthz"},
}
@app.get("/home", include_in_schema=False)
async def home_redirect():
return RedirectResponse(url="/docs", status_code=302)
return app
app = create_app()