from __future__ import annotations import os import requests import yaml from fastapi import Depends, FastAPI, HTTPException from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.exc import SQLAlchemyError app = FastAPI(title="AI CI/CD Assistant") def _env(name: str, default: str) -> str: value = os.getenv(name) return value if value else default # Example for local Ollama: http://localhost:11434/api/generate OLLAMA_API_URL = _env("OLLAMA_API_URL", "http://host.docker.internal:11434/api/generate") REQUIRE_AUTH_FOR_GENERATE = _env("REQUIRE_AUTH_FOR_GENERATE", "true").lower() in ("1", "true", "yes", "on") from backend.auth import create_access_token, get_current_user, hash_password, verify_password # noqa: E402 from backend.db import engine, get_db # noqa: E402 from backend.models import Base, User # noqa: E402 def _password_ok_or_400(password: str): # bcrypt has a 72-byte input limit (bytes, not chars) if len(password.encode("utf-8")) > 72: raise HTTPException(status_code=400, detail="Password is too long (max 72 bytes for bcrypt)") class GenerationRequest(BaseModel): description: str platform: str = "GitHub Actions" # or GitLab CI model: str = "codellama" def validate_config(config_text: str): try: yaml.safe_load(config_text) return True, "Success" except Exception as e: return False, str(e) @app.get("/health") async def health(): # lightweight DB probe to surface connection issues early db_ok = True db_error = None try: with engine.connect() as conn: conn.exec_driver_sql("SELECT 1") except Exception as e: db_ok = False db_error = str(e) return {"status": "ok", "db_ok": db_ok, "db_error": db_error} @app.on_event("startup") def _startup(): # Ensure tables exist; if DB is unreachable, endpoints will surface the error. try: Base.metadata.create_all(bind=engine) except Exception: # Avoid crashing the service hard; /health will show db_error. pass class RegisterRequest(BaseModel): email: str password: str full_name: str | None = None class LoginRequest(BaseModel): email: str password: str @app.post("/auth/register") def register(req: RegisterRequest, db: Session = Depends(get_db)): email = req.email.strip().lower() if not email or not req.password: raise HTTPException(status_code=400, detail="Email and password are required") _password_ok_or_400(req.password) try: exists = db.execute(select(User).where(User.email == email)).scalar_one_or_none() if exists: raise HTTPException(status_code=409, detail="Email already registered") user = User( email=email, full_name=req.full_name.strip() if req.full_name else None, password_hash=hash_password(req.password), is_active=True, is_admin=False, ) db.add(user) db.commit() db.refresh(user) return {"id": str(user.id), "email": user.email, "full_name": user.full_name, "is_admin": user.is_admin} except HTTPException: raise except SQLAlchemyError as e: db.rollback() raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") @app.post("/auth/login") def login(req: LoginRequest, db: Session = Depends(get_db)): email = req.email.strip().lower() if not req.password: raise HTTPException(status_code=400, detail="Password is required") _password_ok_or_400(req.password) try: user = db.execute(select(User).where(User.email == email)).scalar_one_or_none() if not user or not verify_password(req.password, user.password_hash): raise HTTPException(status_code=401, detail="Invalid credentials") if not user.is_active: raise HTTPException(status_code=403, detail="User is inactive") token = create_access_token(user_id=str(user.id)) return {"access_token": token, "token_type": "bearer"} except HTTPException: raise except SQLAlchemyError as e: raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") @app.get("/auth/me") def me(user: User = Depends(get_current_user)): return {"id": str(user.id), "email": user.email, "full_name": user.full_name, "is_admin": user.is_admin} @app.post("/generate") async def generate_pipeline( req: GenerationRequest, user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None, ): prompt = ( f"Write only a valid {req.platform} YAML configuration for: {req.description}. " "No explanations, just code." ) payload = {"model": req.model, "prompt": prompt, "stream": False} try: response = requests.post(OLLAMA_API_URL, json=payload, timeout=60) response.raise_for_status() raw_text = response.json().get("response", "") clean_code = raw_text.replace("```yaml", "").replace("```", "").strip() is_valid, error_msg = validate_config(clean_code) return { "status": "ok" if is_valid else "error", "config": clean_code, "validation_error": error_msg if not is_valid else None, } except Exception as e: raise HTTPException(status_code=500, detail=f"Ollama error: {str(e)}") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=int(_env("PORT", "8000")))