buildai / main.py
VISHAL18for4's picture
Upload 3 files
b69eb48 verified
Raw
History Blame Contribute Delete
11 kB
"""
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 ──────────────────────────────────────────────────
@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"))