Spaces:
Running
Running
File size: 10,051 Bytes
a55ea57 a29f872 a55ea57 2f1e7ff 89e57b7 2f1e7ff a55ea57 3a611e4 a55ea57 3a611e4 a55ea57 3a611e4 a55ea57 2f1e7ff a55ea57 2f1e7ff a55ea57 03c130e a55ea57 03c130e a55ea57 89e57b7 a55ea57 2f1e7ff a55ea57 89e57b7 a55ea57 a29f872 a55ea57 03c130e 53469d7 a55ea57 03c130e | 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | 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)
|