autoapp-builder / app /main.py
ruslanmv's picture
feat: complete AutoApp Builder - AI-powered HF Space generator
2c304fc verified
import os
import io
import json
import uuid
import zipfile
import traceback
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.engine.app_planner import AppPlanner
from app.engine.model_recommender import ModelRecommender
from app.codegen.repo_generator import RepoGenerator
from app.validators.code_checker import CodeChecker
app = FastAPI(title="AutoApp Builder", version="1.0.0")
BASE_DIR = Path(__file__).resolve().parent
GENERATED_DIR = Path("/tmp/autoapp_generated")
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
# In-memory store for generated projects (keyed by session id)
projects: dict = {}
planner = AppPlanner()
recommender = ModelRecommender()
generator = RepoGenerator()
checker = CodeChecker()
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
examples = [
{
"title": "Image Classifier",
"prompt": "Build a Gradio app that classifies images using a pretrained ResNet model. Users upload an image and get top-5 predictions with confidence bars.",
},
{
"title": "AI Chatbot",
"prompt": "Create a chatbot that uses a Hugging Face language model to have conversations. Include chat history, system prompt configuration, and a clear button.",
},
{
"title": "Text Summarizer",
"prompt": "Build an app that summarizes long text documents. Let users paste text or upload a .txt file, choose summary length (short/medium/long), and display the result.",
},
{
"title": "Sentiment Dashboard",
"prompt": "Create an interactive sentiment analysis tool. Users enter text and see sentiment scores (positive/negative/neutral) visualized with charts.",
},
{
"title": "REST API Service",
"prompt": "Build a Docker-based REST API that serves a text generation model with endpoints for completion, summarization, and translation. Include API docs.",
},
{
"title": "Portfolio Site",
"prompt": "Create a beautiful static portfolio website for a data scientist. Include sections for projects, skills, publications, and contact info with a dark theme.",
},
]
return templates.TemplateResponse(
"home.html", {"request": request, "examples": examples}
)
@app.post("/generate", response_class=HTMLResponse)
async def generate(
request: Request,
prompt: str = Form(...),
sdk_preference: str = Form("auto"),
model_size: str = Form("medium"),
gpu_needed: bool = Form(False),
features: str = Form(""),
):
try:
# Step 1: Plan the app
plan = planner.analyze(prompt, sdk_preference)
# Step 2: Recommend models
recommended_models = recommender.recommend(plan, model_size, gpu_needed)
plan["recommended_models"] = recommended_models
# Step 3: Parse additional features
feature_list = [f.strip() for f in features.split(",") if f.strip()]
plan["extra_features"] = feature_list
# Step 4: Generate repository files
repo_files = generator.generate(plan, prompt)
# Step 5: Validate the generated code
validation = checker.check(repo_files, plan["sdk"])
# Step 6: Store the project
project_id = str(uuid.uuid4())[:8]
projects[project_id] = {
"plan": plan,
"files": repo_files,
"validation": validation,
"prompt": prompt,
}
# Build file tree structure
file_tree = _build_file_tree(repo_files)
# Generate architecture diagram
arch_diagram = _generate_arch_diagram(plan)
return templates.TemplateResponse(
"result.html",
{
"request": request,
"project_id": project_id,
"plan": plan,
"files": repo_files,
"file_tree": file_tree,
"validation": validation,
"arch_diagram": arch_diagram,
"prompt": prompt,
},
)
except Exception as e:
traceback.print_exc()
return templates.TemplateResponse(
"home.html",
{
"request": request,
"examples": [],
"error": f"Generation failed: {str(e)}. Please try again with a different prompt.",
},
)
@app.post("/edit", response_class=HTMLResponse)
async def edit_project(
request: Request,
project_id: str = Form(...),
edit_prompt: str = Form(...),
):
if project_id not in projects:
raise HTTPException(status_code=404, detail="Project not found")
project = projects[project_id]
original_plan = project["plan"]
original_files = project["files"]
try:
# Re-generate with edit instructions
updated_files = generator.edit(original_plan, original_files, edit_prompt)
validation = checker.check(updated_files, original_plan["sdk"])
project["files"] = updated_files
project["validation"] = validation
file_tree = _build_file_tree(updated_files)
arch_diagram = _generate_arch_diagram(original_plan)
return templates.TemplateResponse(
"result.html",
{
"request": request,
"project_id": project_id,
"plan": original_plan,
"files": updated_files,
"file_tree": file_tree,
"validation": validation,
"arch_diagram": arch_diagram,
"prompt": project["prompt"],
"edit_prompt": edit_prompt,
},
)
except Exception as e:
traceback.print_exc()
# Return original project with error
file_tree = _build_file_tree(original_files)
arch_diagram = _generate_arch_diagram(original_plan)
return templates.TemplateResponse(
"result.html",
{
"request": request,
"project_id": project_id,
"plan": original_plan,
"files": original_files,
"file_tree": file_tree,
"validation": project["validation"],
"arch_diagram": arch_diagram,
"prompt": project["prompt"],
"edit_error": f"Edit failed: {str(e)}",
},
)
@app.get("/download/{project_id}")
async def download_zip(project_id: str):
if project_id not in projects:
raise HTTPException(status_code=404, detail="Project not found")
project = projects[project_id]
files = project["files"]
plan = project["plan"]
app_name = plan.get("app_name", "my-hf-space")
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for filepath, content in files.items():
zf.writestr(f"{app_name}/{filepath}", content)
buf.seek(0)
return StreamingResponse(
buf,
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{app_name}.zip"'},
)
@app.get("/api/file/{project_id}/{filepath:path}")
async def get_file(project_id: str, filepath: str):
if project_id not in projects:
raise HTTPException(status_code=404, detail="Project not found")
files = projects[project_id]["files"]
if filepath not in files:
raise HTTPException(status_code=404, detail="File not found")
return JSONResponse({"filename": filepath, "content": files[filepath]})
def _build_file_tree(files: dict) -> list:
"""Build a nested file tree structure from flat file dict."""
tree = []
dirs_seen = set()
sorted_files = sorted(files.keys())
for filepath in sorted_files:
parts = filepath.split("/")
# Add directory entries
for i in range(len(parts) - 1):
dir_path = "/".join(parts[: i + 1])
if dir_path not in dirs_seen:
dirs_seen.add(dir_path)
tree.append(
{
"path": dir_path,
"name": parts[i],
"type": "dir",
"depth": i,
}
)
# Add file entry
tree.append(
{
"path": filepath,
"name": parts[-1],
"type": "file",
"depth": len(parts) - 1,
}
)
return tree
def _generate_arch_diagram(plan: dict) -> str:
"""Generate ASCII architecture diagram."""
sdk = plan.get("sdk", "gradio")
app_name = plan.get("app_name", "App")
components = plan.get("components", [])
if sdk == "gradio":
diagram = f"""
+------------------------------------------------------+
| Hugging Face Spaces |
| +------------------------------------------------+ |
| | {app_name:^30s} | |
| | +------------------------------------------+ | |
| | | Gradio Interface | | |
| | | +------------+ +------------------+ | | |
| | | | Inputs |--->| Processing | | | |
| | | +------------+ | +------------+ | | | |
| | | | | HF Model | | | | |
| | | +------------+ | +------------+ | | | |
| | | | Outputs |<---| | | | |
| | | +------------+ +------------------+ | | |
| | +------------------------------------------+ | |
| +------------------------------------------------+ |
+------------------------------------------------------+"""
elif sdk == "docker":
diagram = f"""
+------------------------------------------------------+
| Hugging Face Spaces |
| +------------------------------------------------+ |
| | Docker Container | |
| | +------------------------------------------+ | |
| | | {app_name:^30s} | | |
| | | +----------+ +----------+ +---------+ | | |
| | | | FastAPI | | Model | | Utils | | | |
| | | | Routes | | Service | | | | | |
| | | +-----+-----+ +----+-----+ +---------+ | | |
| | | | | | | |
| | | +-----v--------------v-----------------+ | | |
| | | | API Endpoints | | | |
| | | +---------------------------------------+ | | |
| | +------------------------------------------+ | |
| +------------------------------------------------+ |
+------------------------------------------------------+"""
else:
diagram = f"""
+------------------------------------------------------+
| Hugging Face Spaces |
| +------------------------------------------------+ |
| | Static Site | |
| | +------------------------------------------+ | |
| | | {app_name:^30s} | | |
| | | +----------+ +----------+ +---------+ | | |
| | | | HTML | | CSS | | JS | | | |
| | | | Pages | | Styles | | Scripts | | | |
| | | +----------+ +----------+ +---------+ | | |
| | +------------------------------------------+ | |
| +------------------------------------------------+ |
+------------------------------------------------------+"""
return diagram