import os os.environ["NUMEXPR_MAX_THREADS"] = "12" from fastapi import FastAPI, HTTPException, Depends, Body, status, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles from fastapi.security import OAuth2PasswordRequestForm from contextlib import asynccontextmanager from typing import Dict, Any import logging from logging.handlers import RotatingFileHandler from datetime import timedelta import pathlib from modules.configuration import APP_CONFIG def initLogging(): # Get log level from config (default to INFO if not found) logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING") logLevel = getattr(logging, logLevelName) # Configure handlers based on config handlers = [] # Add console handler if enabled if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True): consoleHandler = logging.StreamHandler() handlers.append(consoleHandler) # Add file handler if enabled if APP_CONFIG.get("APP_LOGGING_FILE_ENABLED", True): logFile = APP_CONFIG.get("APP_LOGGING_LOG_FILE", "app.log") rotationSize = int(APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760)) # Default: 10MB backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5)) fileHandler = RotatingFileHandler( logFile, maxBytes=rotationSize, backupCount=backupCount ) handlers.append(fileHandler) # Configure the logger logging.basicConfig( level=logLevel, format=APP_CONFIG.get("APP_LOGGING_FORMAT", "%(asctime)s - %(levelname)s - %(name)s - %(message)s"), datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S"), handlers=handlers ) # Silence noisy third-party libraries - use the same level as the root logger noisyLoggers = ["httpx", "urllib3", "asyncio", "fastapi.security.oauth2"] for loggerName in noisyLoggers: logging.getLogger(loggerName).setLevel(logLevel) # Initialize logging initLogging() logger = logging.getLogger(__name__) instanceLabel = APP_CONFIG.get("APP_ENV_LABEL") # Import auth module from modules.auth import ( createAccessToken, getCurrentActiveUser, getUserContext, ACCESS_TOKEN_EXPIRE_MINUTES ) # Import models - import generically for INITIALIZATION import modules.gatewayModel as gatewayModel from modules.gatewayInterface import getGatewayInterface gateway = getGatewayInterface() # Define lifespan context manager for application startup/shutdown events @asynccontextmanager async def lifespan(app: FastAPI): # Startup logic (if any) logger.info("Application is starting up") yield # Shutdown logic logger.info("Application has been shut down") # Parse CORS origins from environment variable def get_allowed_origins(): origins_str = APP_CONFIG.get("APP_ALLOWED_ORIGINS", "http://localhost:8080") # Split by comma and strip whitespace origins = [origin.strip() for origin in origins_str.split(",")] logger.info(f"CORS allowed origins: {origins}") return origins # START APP app = FastAPI( title="PowerOn | Data Platform API", description=f"Backend API for the Multi-Agent Platform by ValueOn AG ({instanceLabel})", lifespan=lifespan ) # CORS configuration using environment variables app.add_middleware( CORSMiddleware, allow_origins= get_allowed_origins(), # ["http://localhost:8080","http://localhost:8081"], #get_allowed_origins(), allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], expose_headers=["*"], max_age=86400 # Increased caching for preflight requests ) # Static folder for frontend - work with absolute path baseDir = pathlib.Path(__file__).parent staticFolder = baseDir / "static" os.makedirs(staticFolder, exist_ok=True) app.mount("/static", StaticFiles(directory=str(staticFolder)), name="static") # General Elements @app.get("/", tags=["General"]) async def root(): """API status endpoint""" return {"status": "online", "message": "Data Platform API is active"} @app.get("/api/test", tags=["General"]) async def getTest(): return f"Status: OK. Alowed origins: {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}" @app.options("/{fullPath:path}", tags=["General"]) async def optionsRoute(fullPath: str): return Response(status_code=200) @app.get("/api/environment", tags=["General"]) async def get_environment(): """Get environment configuration for frontend""" return { "apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""), "environment": APP_CONFIG.get("APP_ENV", "development"), "instanceLabel": APP_CONFIG.get("APP_ENV_LABEL", "Development"), # Add other environment variables the frontend might need } # Token endpoint for login @app.post("/api/token", response_model=gatewayModel.Token, tags=["General"]) async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()): # Initialize Gateway interface without context gateway = getGatewayInterface() # Authenticate user user = gateway.authenticateUser(formData.username, formData.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password", headers={"WWW-Authenticate": "Bearer"}, ) # Create token with tenant ID accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) accessToken = createAccessToken( data={ "sub": user["username"], "mandateId": user["mandateId"] }, expiresDelta=accessTokenExpires ) return {"accessToken": accessToken, "tokenType": "bearer"} # Get user info @app.get("/api/user/me", response_model=Dict[str, Any], tags=["General"]) async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): return currentUser # Include all routers from routes.routeAttributes import router as attributesRouter app.include_router(attributesRouter) gateway = getGatewayInterface() from routes.routeMandates import router as mandateRouter app.include_router(mandateRouter) gateway = getGatewayInterface() from routes.routeUsers import router as userRouter app.include_router(userRouter) from routes.routeFiles import router as fileRouter app.include_router(fileRouter) from routes.routePrompts import router as promptRouter app.include_router(promptRouter) from routes.routeWorkflows import router as workflowRouter app.include_router(workflowRouter) #if __name__ == "__main__": # uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)