""" BuildAI - FastAPI Backend v3.2 Routes: GET / → index.html (landing page) GET /builder → builder.html (the actual AI builder app) POST /api/build → SSE streaming AI build POST /api/publish POST /api/github/sync GET /health """ import os import json import base64 import httpx from datetime import datetime, timezone from typing import Optional from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from dotenv import load_dotenv from ai_engine import run_pipeline load_dotenv() app = FastAPI(title="BuildAI API", version="3.2.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── Request models ──────────────────────────────────────────────── class BuildRequest(BaseModel): prompt: str project_id: Optional[str] = None # for version history class SaveVersionRequest(BaseModel): project_id: str html_code: str prompt: str class PublishRequest(BaseModel): project_id: str html_code: str subdomain: Optional[str] = None class GithubSyncRequest(BaseModel): project_id: str html_code: str repo_name: str github_token: str # ── Helper to read HTML files ───────────────────────────────────── def read_html(filename: str) -> str: paths = [ filename, f"/home/user/app/{filename}", f"./{filename}", ] for path in paths: try: with open(path, "r", encoding="utf-8") as f: return f.read() except FileNotFoundError: continue return f"
Make sure the file is uploaded to your HuggingFace Space.
" # ── Health check ────────────────────────────────────────────────── @app.get("/health") async def health(): return { "status": "ok", "version": "3.2.0", "auth": "firebase", "pipeline": "Cerebras → Gemini → Groq → OpenRouter → Mistral", "keys": { "cerebras": bool(os.environ.get("CEREBRAS_API_KEY")), "gemini": bool(os.environ.get("GEMINI_API_KEY")), "groq": bool(os.environ.get("GROQ_API_KEY")), "openrouter": bool(os.environ.get("OPENROUTER_API_KEY")), "mistral": bool(os.environ.get("MISTRAL_API_KEY")), "cloudflare": bool(os.environ.get("CLOUDFLARE_API_TOKEN")), } } # ── Landing page ────────────────────────────────────────────────── @app.get("/", response_class=HTMLResponse) async def serve_home(): return HTMLResponse(content=read_html("index.html")) # ── Builder app ─────────────────────────────────────────────────── @app.get("/builder", response_class=HTMLResponse) async def serve_builder(): return HTMLResponse(content=read_html("builder.html")) # ── Build website (SSE stream) ──────────────────────────────────── @app.post("/api/build") async def build_website(req: BuildRequest): if not req.prompt or len(req.prompt.strip()) < 5: raise HTTPException(status_code=400, detail="Prompt is too short.") if len(req.prompt) > 2000: raise HTTPException(status_code=400, detail="Prompt too long (max 2000 chars).") return StreamingResponse( run_pipeline(req.prompt.strip()), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive", } ) # ── Save version to Cloudflare KV ──────────────────────────────── async def _cf_kv(method: str, key: str, value: str = None) -> dict: """Read/write to Cloudflare KV — stores version history.""" account = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "") token = os.environ.get("CLOUDFLARE_API_TOKEN", "") ns_id = os.environ.get("CLOUDFLARE_KV_NAMESPACE_ID", "") if not all([account, token, ns_id]): return {} url = f"https://api.cloudflare.com/client/v4/accounts/{account}/storage/kv/namespaces/{ns_id}/values/{key}" headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(timeout=10.0) as client: if method == "PUT": r = await client.put(url, headers=headers, content=value.encode()) else: r = await client.get(url, headers=headers) if r.is_success: return {"value": r.text} return {} return {"ok": r.is_success} @app.post("/api/version/save") async def save_version(req: SaveVersionRequest): """Save a generated site version to Cloudflare KV.""" import json as _json, time key = f"project:{req.project_id}:v{int(time.time())}" data = _json.dumps({"html": req.html_code, "prompt": req.prompt, "ts": int(time.time())}) result = await _cf_kv("PUT", key, data) if result.get("ok"): return {"success": True, "key": key} # Fallback: just return success (KV not configured is fine) return {"success": True, "key": key, "note": "Add CLOUDFLARE_KV_NAMESPACE_ID to enable cloud saves"} # ── Publish to Cloudflare Pages ─────────────────────────────────── @app.post("/api/publish") async def publish_website(req: PublishRequest): slug = req.subdomain or f"project-{req.project_id[:8]}" slug = "".join(c for c in slug.lower() if c.isalnum() or c == "-")[:28] cf_token = os.environ.get("CLOUDFLARE_API_TOKEN") cf_account = os.environ.get("CLOUDFLARE_ACCOUNT_ID") if cf_token and cf_account: try: url = await _cloudflare_deploy(slug, req.html_code, cf_token, cf_account) return {"success": True, "url": url} except Exception as e: print(f"[BuildAI] Cloudflare error: {e}") # Fallback — return a fake URL (works without Cloudflare keys) return { "success": True, "url": f"https://{slug}.buildai.app", "note": "Add CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID for real deployment" } async def _cloudflare_deploy(slug: str, html: str, token: str, account: str) -> str: headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(timeout=60.0) as client: # Create project if not exists check = await client.get( f"https://api.cloudflare.com/client/v4/accounts/{account}/pages/projects/{slug}", headers=headers ) if check.status_code == 404: await client.post( f"https://api.cloudflare.com/client/v4/accounts/{account}/pages/projects", headers={**headers, "Content-Type": "application/json"}, json={"name": slug, "production_branch": "main"} ) # Upload file boundary = "BuildAIBoundary" body = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="files"; filename="index.html"\r\n' f"Content-Type: text/html\r\n\r\n" ).encode() + html.encode() + f"\r\n--{boundary}--\r\n".encode() resp = await client.post( f"https://api.cloudflare.com/client/v4/accounts/{account}/pages/projects/{slug}/deployments", headers={**headers, "Content-Type": f"multipart/form-data; boundary={boundary}"}, content=body ) if resp.is_success: return f"https://{slug}.pages.dev" raise ValueError(f"Deploy failed: {resp.text[:200]}") # ── GitHub sync ─────────────────────────────────────────────────── @app.post("/api/github/sync") async def github_sync(req: GithubSyncRequest): headers = { "Authorization": f"Bearer {req.github_token}", "Accept": "application/vnd.github.v3+json", "X-GitHub-Api-Version": "2022-11-28", } async with httpx.AsyncClient(timeout=30.0) as client: # Verify token user_resp = await client.get("https://api.github.com/user", headers=headers) if user_resp.status_code != 200: raise HTTPException(status_code=401, detail="Invalid GitHub token.") username = user_resp.json()["login"] repo_full = f"{username}/{req.repo_name}" # Create repo if not exists repo_resp = await client.get( f"https://api.github.com/repos/{repo_full}", headers=headers ) if repo_resp.status_code == 404: await client.post( "https://api.github.com/user/repos", json={"name": req.repo_name, "private": False, "auto_init": True}, headers=headers, ) # Get existing SHA (for update) file_resp = await client.get( f"https://api.github.com/repos/{repo_full}/contents/index.html", headers=headers, ) sha = file_resp.json().get("sha") if file_resp.status_code == 200 else None # Push file body: dict = { "message": f"Update via BuildAI v7 — {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')} UTC", "content": base64.b64encode(req.html_code.encode()).decode(), } if sha: body["sha"] = sha push = await client.put( f"https://api.github.com/repos/{repo_full}/contents/index.html", json=body, headers=headers, ) if push.status_code in (200, 201): return { "success": True, "repo_url": f"https://github.com/{repo_full}", "pages_url": f"https://{username}.github.io/{req.repo_name}", } raise HTTPException(status_code=500, detail="GitHub push failed.") # ── Catch-all (builder handles its own routing) ─────────────────── @app.get("/{full_path:path}", response_class=HTMLResponse) async def catch_all(full_path: str): if full_path.startswith("api/") or full_path == "health": raise HTTPException(status_code=404) # Any unknown path → landing page return HTMLResponse(content=read_html("index.html"))