File size: 6,438 Bytes
215df55 e465928 fed7eb0 874d7cd e465928 215df55 d62044b 215df55 d62044b 215df55 d62044b 215df55 d62044b 215df55 d62044b 215df55 d62044b 215df55 874d7cd 215df55 fed7eb0 215df55 e465928 d62044b e465928 d62044b e465928 d62044b 6338f31 215df55 6338f31 215df55 d62044b 6338f31 d62044b e465928 215df55 e465928 215df55 e465928 d62044b fed7eb0 e465928 fed7eb0 e465928 215df55 fed7eb0 215df55 fed7eb0 e465928 215df55 e465928 215df55 e465928 874d7cd e465928 215df55 e465928 215df55 fed7eb0 215df55 |
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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# 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()
|