Spaces:
Sleeping
Sleeping
| """ | |
| 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"<h1>{filename} not found</h1><p>Make sure the file is uploaded to your HuggingFace Space.</p>" | |
| # ββ Health check ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def serve_home(): | |
| return HTMLResponse(content=read_html("index.html")) | |
| # ββ Builder app βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def serve_builder(): | |
| return HTMLResponse(content=read_html("builder.html")) | |
| # ββ Build website (SSE stream) ββββββββββββββββββββββββββββββββββββ | |
| 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} | |
| 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 βββββββββββββββββββββββββββββββββββ | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) βββββββββββββββββββ | |
| 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")) | |