|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
try: |
|
|
from dotenv import load_dotenv |
|
|
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: |
|
|
|
|
|
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() |
|
|
|
|
|
if (val.startswith('"') and val.endswith('"')) or ( |
|
|
val.startswith("'") and val.endswith("'") |
|
|
): |
|
|
val = val[1:-1] |
|
|
|
|
|
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)") |
|
|
|
|
|
|
|
|
_load_env_file([".env", "configs/.env", ".env.local", "configs/.env.local"]) |
|
|
|
|
|
|
|
|
|
|
|
from .deps import get_settings |
|
|
from .services.chat_service import get_retriever |
|
|
from .core.rag.build import ensure_kb |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
from .middleware import attach_middlewares |
|
|
except Exception: |
|
|
try: |
|
|
from .middlewares import attach_middlewares |
|
|
except Exception: |
|
|
def attach_middlewares(app: FastAPI) -> None: |
|
|
logging.getLogger("uvicorn.error").warning( |
|
|
"attach_middlewares not found; continuing without custom middlewares." |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
from .routers import health, plan, chat |
|
|
|
|
|
|
|
|
try: |
|
|
from .ui import router as ui_router |
|
|
HAS_UI = True |
|
|
except Exception: |
|
|
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") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
logger.info("Warming up RAG retriever...") |
|
|
get_retriever(get_settings()) |
|
|
logger.info("RAG retriever is ready.") |
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
attach_middlewares(app) |
|
|
|
|
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
if HAS_UI: |
|
|
app.include_router(ui_router, tags=["UI"]) |
|
|
else: |
|
|
|
|
|
@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() |
|
|
|