brain / src /services /api /app.py
Leon4gr45's picture
Deploy complete BrainAPI source 257d17ff652a
a29f872 verified
Raw History Blame Contribute Delete
10.1 kB
import logging
import os
import re
from hashlib import sha1
from contextlib import asynccontextmanager
from pathlib import Path
import dotenv
_project_root = Path(__file__).resolve().parent.parent.parent.parent
dotenv.load_dotenv(_project_root / ".env")
from fastapi import FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from fastapi.routing import APIRoute
from uvicorn import run
from src.constants.data import BRAIN_VERSION
from src.services.api.console_static import SPAStaticFiles
from src.services.api.connection_docs import connection_docs_router
from src.services.api.errors import install_error_handlers
from src.services.api.openapi import install_openapi_contract
from src.services.api.middlewares.auth import BrainPATMiddleware
from src.services.api.middlewares.brains import BrainMiddleware
from src.services.api.routes.auth import auth_router
from src.services.api.routes.backup import backup_router
from src.services.api.routes.access_tokens import access_token_router
from src.services.api.routes.ingest import ingest_router
from src.services.api.routes.meta import meta_router
from src.services.api.routes.model import model_router
from src.services.api.routes.public import public_router
from src.services.api.routes.retrieve import retrieve_router
from src.services.api.routes.system import system_router
from src.services.api.routes.tasks import tasks_router
from src.services.api.routes.workspaces import workspace_router
from src.services.api.routes.workspace_api import workspace_api_router
from src.services.api.routes.agent import agent_router
from src.lib.tracing.middleware import TraceMiddleware
from src.lib.tracing.runtime import start_runtime_monitoring, stop_runtime_monitoring
logger = logging.getLogger("brainapi.plugins")
PLUGINS_DIR = Path(os.getenv("PLUGINS_DIR", str(_project_root / "plugins")))
CONSOLE_DIST = _project_root / "console" / "dist"
def _console_enabled() -> bool:
return os.getenv("CONSOLE_ENABLED", "true").strip().lower() != "false"
def _production_mode() -> bool:
return os.getenv("ENV", "production").strip().lower() != "development"
def _cors_allowed_origins() -> list[str]:
configured = [
origin.strip()
for origin in os.getenv("CORS_ALLOWED_ORIGINS", "").split(",")
if origin.strip()
]
if not configured and not _production_mode():
return ["*"]
if "*" in configured and _production_mode():
raise RuntimeError(
"CORS_ALLOWED_ORIGINS='*' is allowed only when ENV=development"
)
return configured
def _enforce_plugin_results(results: dict[str, bool]) -> None:
failed = sorted(name for name, loaded in results.items() if not loaded)
if not failed:
return
default_policy = "fail" if _production_mode() else "warn"
policy = os.getenv("PLUGIN_FAILURE_POLICY", default_policy).strip().lower()
if policy not in {"fail", "warn"}:
raise RuntimeError("PLUGIN_FAILURE_POLICY must be 'fail' or 'warn'")
if policy == "fail":
raise RuntimeError(f"Required plugins failed to load: {', '.join(failed)}")
def stable_operation_id(route: APIRoute) -> str:
"""Generate function-calling-safe IDs that are stable across schema exports."""
tag = route.tags[0] if route.tags else "api"
method = sorted(route.methods or {"get"})[0].lower()
raw = f"{tag}_{route.name}_{method}".lower()
normalized = re.sub(r"[^a-z0-9_]+", "_", raw).strip("_")
if len(normalized) <= 64:
return normalized
digest = sha1(normalized.encode("utf-8")).hexdigest()[:8]
return f"{normalized[:55].rstrip('_')}_{digest}"
@asynccontextmanager
async def lifespan(app: FastAPI):
from src.services.brain_backup.state import require_startup_ready
require_startup_ready()
from src.core.plugins.context import PluginContext
from src.core.plugins.loader import PluginLoader
from src.services.persistence import enforce_persistence
enforce_persistence()
from src.services.auth_config import resolve_system_pat
resolve_system_pat()
from src.config import config
if config.data_db == "postgresql":
from src.services.credentials import token_repository
token_repository.initialize()
start_runtime_monitoring("brainapi-api")
ctx = PluginContext.from_app(app)
loader = PluginLoader(plugins_dir=PLUGINS_DIR, context=ctx)
results = loader.load_all()
try:
from src.services.data.main import data_adapter
from src.services.workspaces import WorkspaceService
WorkspaceService(data_adapter).bootstrap()
except Exception:
logger.exception("Workspace bootstrap deferred; persistence is unavailable")
_log_plugin_banner(loader, results)
_enforce_plugin_results(results)
for event_name, handlers in ctx._event_handlers.items():
if event_name == "startup":
for handler in handlers:
await handler() if _is_coroutine(handler) else handler()
try:
yield
finally:
stop_runtime_monitoring("brainapi-api")
for event_name, handlers in ctx._event_handlers.items():
if event_name == "shutdown":
for handler in handlers:
await handler() if _is_coroutine(handler) else handler()
def _is_coroutine(fn):
import asyncio
return asyncio.iscoroutinefunction(fn)
def _log_plugin_banner(loader, results: dict[str, bool]):
loaded = loader.loaded_plugins
total = len(results)
ok = sum(1 for v in results.values() if v)
failed = total - ok
lines = [
"",
"\033[36m ╔══════════════════════════════════════════════════════╗\033[0m",
"\033[36m β•‘\033[0m \033[1;36m⚑ BrainAPI Plugin System ⚑\033[0m \033[36mβ•‘\033[0m",
"\033[36m ╠══════════════════════════════════════════════════════╣\033[0m",
]
if total == 0:
lines.append(
"\033[36m β•‘\033[0m \033[2mNo plugins installed\033[0m \033[36mβ•‘\033[0m"
)
else:
for name, success in results.items():
manifest = loaded.get(name)
if success and manifest:
ver = f"v{manifest.version}"
status = "\033[32mβœ” loaded\033[0m"
label = f"{manifest.name} ({ver})"
else:
status = "\033[31m✘ failed\033[0m"
label = name
padded = f" {status} {label}"
visible_len = len(f" βœ” loaded {label}")
pad = 54 - visible_len
lines.append(f"\033[36m β•‘\033[0m{padded}{' ' * max(pad, 1)}\033[36mβ•‘\033[0m")
lines.append("\033[36m ╠══════════════════════════════════════════════════════╣\033[0m")
summary_parts = [f"\033[1;32m{ok} loaded\033[0m"]
if failed:
summary_parts.append(f"\033[1;31m{failed} failed\033[0m")
summary_text = f" {' Β· '.join(summary_parts)}"
visible_summary_len = len(f" {ok} loaded" + (f" Β· {failed} failed" if failed else ""))
summary_pad = 54 - visible_summary_len
lines.append(f"\033[36m β•‘\033[0m{summary_text}{' ' * max(summary_pad, 1)}\033[36mβ•‘\033[0m")
lines.append("\033[36m β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\033[0m")
lines.append("")
print("\n".join(lines))
app = FastAPI(
title="BrainAPI",
version=BRAIN_VERSION,
description="Knowledge, memory, search, recommendation, and agent API.",
debug=os.getenv("ENV") == "development",
lifespan=lifespan,
redirect_slashes=False,
generate_unique_id_function=stable_operation_id,
)
install_error_handlers(app)
from src.services.brain_backup.middleware import ContentAdmissionMiddleware
app.add_middleware(ContentAdmissionMiddleware)
app.add_middleware(BrainPATMiddleware)
app.add_middleware(BrainMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_allowed_origins(),
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(TraceMiddleware, service_name="brainapi-api")
app.include_router(auth_router)
app.include_router(access_token_router)
app.include_router(ingest_router)
app.include_router(retrieve_router)
app.include_router(meta_router)
app.include_router(model_router)
app.include_router(system_router)
app.include_router(backup_router)
app.include_router(tasks_router)
app.include_router(public_router)
app.include_router(connection_docs_router)
app.include_router(workspace_router)
app.include_router(workspace_api_router)
app.include_router(agent_router)
install_openapi_contract(app)
@app.get("/", include_in_schema=False)
async def root():
return RedirectResponse(url="/console/", status_code=307)
if _console_enabled():
@app.get("/console", include_in_schema=False)
async def console_redirect():
return RedirectResponse(url="/console/", status_code=307)
if CONSOLE_DIST.is_dir():
app.mount(
"/console",
SPAStaticFiles(directory=str(CONSOLE_DIST), html=True),
name="console",
)
logger.info("Local console mounted at /console")
else:
logger.warning(
"Console enabled but %s not found β€” run: make build-console",
CONSOLE_DIST,
)
elif os.getenv("CONSOLE_ENABLED", "").strip().lower() == "true":
logger.warning(
"CONSOLE_ENABLED=true but console is disabled β€” set CONSOLE_ENABLED=false to silence",
)
if __name__ == "__main__":
run(app, host="0.0.0.0", port=8000)