|
|
"""
|
|
|
LLM Code Deployment - FastAPI Backend
|
|
|
Automates app building and deployment using LLMs and GitHub Pages
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
import logging
|
|
|
from typing import Dict, Any, Optional
|
|
|
from datetime import datetime
|
|
|
|
|
|
from fastapi import FastAPI, HTTPException, status
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.responses import JSONResponse
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
from llm_helper import LLMHelper, AppGenerationRequest
|
|
|
from github_helper import GitHubHelper
|
|
|
from deploy_helper import DeployHelper
|
|
|
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
level=logging.INFO,
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
|
)
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
app = FastAPI(
|
|
|
title="LLM Code Deployment API",
|
|
|
description="Automates app building and deployment using LLMs and GitHub Pages",
|
|
|
version="1.0.0",
|
|
|
docs_url="/docs",
|
|
|
redoc_url="/redoc"
|
|
|
)
|
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
CORSMiddleware,
|
|
|
allow_origins=["*"],
|
|
|
allow_credentials=True,
|
|
|
allow_methods=["*"],
|
|
|
allow_headers=["*"],
|
|
|
)
|
|
|
|
|
|
|
|
|
llm_helper = None
|
|
|
github_helper = None
|
|
|
deploy_helper = None
|
|
|
|
|
|
|
|
|
def get_llm_helper() -> LLMHelper:
|
|
|
global llm_helper
|
|
|
if llm_helper is None:
|
|
|
logger.info("Initializing LLMHelper...")
|
|
|
llm_helper = LLMHelper()
|
|
|
return llm_helper
|
|
|
|
|
|
|
|
|
def get_github_helper() -> GitHubHelper:
|
|
|
global github_helper
|
|
|
if github_helper is None:
|
|
|
logger.info("Initializing GitHubHelper...")
|
|
|
github_helper = GitHubHelper()
|
|
|
return github_helper
|
|
|
|
|
|
|
|
|
def get_deploy_helper() -> DeployHelper:
|
|
|
global deploy_helper
|
|
|
if deploy_helper is None:
|
|
|
logger.info("Initializing DeployHelper...")
|
|
|
deploy_helper = DeployHelper()
|
|
|
return deploy_helper
|
|
|
|
|
|
|
|
|
class TaskRequest(BaseModel):
|
|
|
email: str = Field(..., description="User email address")
|
|
|
secret: str = Field(..., description="Shared secret for authentication")
|
|
|
task: str = Field(..., description="Task description")
|
|
|
round: int = Field(default=1, description="Round number (1 for initial, 2+ for revisions)")
|
|
|
nonce: str = Field(..., description="Unique request identifier")
|
|
|
brief: str = Field(..., description="Detailed task brief")
|
|
|
evaluation_url: str = Field(..., description="URL to send evaluation data")
|
|
|
attachments: Optional[list] = Field(default=None, description="Additional attachments or context")
|
|
|
return_code: Optional[bool] = Field(default=False, description="If true, include generated code in response")
|
|
|
|
|
|
@field_validator('secret', mode='before')
|
|
|
@classmethod
|
|
|
def validate_secret(cls, v):
|
|
|
|
|
|
expected_secret = os.getenv("SHARED_SECRET")
|
|
|
try:
|
|
|
if expected_secret and v != expected_secret:
|
|
|
logger.warning("Shared secret mismatch; proceeding due to relaxed validation")
|
|
|
except Exception:
|
|
|
pass
|
|
|
return v
|
|
|
|
|
|
@field_validator('evaluation_url', mode='before')
|
|
|
@classmethod
|
|
|
def validate_evaluation_url(cls, v):
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
class EvaluationRequest(BaseModel):
|
|
|
email: str = Field(..., description="User email address")
|
|
|
task: str = Field(..., description="Task description")
|
|
|
round: int = Field(..., description="Round number")
|
|
|
nonce: str = Field(..., description="Unique request identifier")
|
|
|
evaluation_data: Dict[str, Any] = Field(..., description="Evaluation results")
|
|
|
timestamp: Optional[str] = Field(default=None, description="Evaluation timestamp")
|
|
|
|
|
|
|
|
|
class HealthResponse(BaseModel):
|
|
|
status: str
|
|
|
timestamp: str
|
|
|
version: str
|
|
|
|
|
|
|
|
|
@app.get("/", response_model=HealthResponse)
|
|
|
async def root():
|
|
|
return HealthResponse(
|
|
|
status="healthy",
|
|
|
timestamp=datetime.utcnow().isoformat(),
|
|
|
version="1.0.0"
|
|
|
)
|
|
|
|
|
|
|
|
|
@app.get("/health", response_model=HealthResponse)
|
|
|
async def health_check():
|
|
|
return HealthResponse(
|
|
|
status="healthy",
|
|
|
timestamp=datetime.utcnow().isoformat(),
|
|
|
version="1.0.0"
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _fallback_generated_app(task: str) -> dict:
|
|
|
title = f"{task or 'LLM App'}"
|
|
|
html = f"""<!doctype html><html><head><meta charset=\"utf-8\"><title>{title}</title><link rel=\"stylesheet\" href=\"styles.css\"></head><body><div id=\"app\"><h1>{title}</h1><p>Your app was generated in fallback mode.</p><script src=\"script.js\"></script></div></body></html>"""
|
|
|
css = "body{font-family:system-ui,Segoe UI,Arial,sans-serif;margin:40px;background:#fafafa;color:#222}#app{max-width:800px;margin:auto;padding:24px;border:1px solid #e5e5e5;border-radius:12px;background:#fff}h1{margin-top:0}"
|
|
|
js = "console.log('Fallback app initialized');"
|
|
|
metadata = {"title": title, "description": "Fallback generated application"}
|
|
|
return {"html_content": html, "css_content": css, "js_content": js, "metadata": metadata}
|
|
|
|
|
|
|
|
|
def _local_repo_deploy(app_name: str, html: str, css: str, js: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
import pathlib
|
|
|
from datetime import datetime as _dt
|
|
|
timestamp = _dt.now().strftime("%Y%m%d%H%M%S")
|
|
|
safe = "".join(c for c in (app_name or 'app') if c.isalnum() or c in ('-','_')).lower() or "app"
|
|
|
repo_name = f"llm-app-{safe}-{timestamp}"
|
|
|
out_dir = pathlib.Path("out") / repo_name
|
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
(out_dir / "index.html").write_text(html, encoding="utf-8")
|
|
|
if css:
|
|
|
(out_dir / "styles.css").write_text(css, encoding="utf-8")
|
|
|
if js:
|
|
|
(out_dir / "script.js").write_text(js, encoding="utf-8")
|
|
|
(out_dir / "README.md").write_text(f"# {metadata.get('title','LLM App')}\n\nLocal fallback deployment.\n", encoding="utf-8")
|
|
|
pages_url = f"file://{(out_dir / 'index.html').resolve()}"
|
|
|
return {
|
|
|
"repo_name": repo_name,
|
|
|
"repo_url": str(out_dir.resolve()),
|
|
|
"commit_sha": "local-fallback",
|
|
|
"pages_url": pages_url,
|
|
|
"success": True,
|
|
|
"fallback": True,
|
|
|
}
|
|
|
|
|
|
|
|
|
@app.post("/api/request")
|
|
|
async def process_request(request: TaskRequest):
|
|
|
try:
|
|
|
logger.info(f"Processing request for {request.email}, round {request.round}")
|
|
|
|
|
|
errors: list[str] = []
|
|
|
|
|
|
logger.info("Generating application with LLM...")
|
|
|
try:
|
|
|
app_request = AppGenerationRequest(
|
|
|
task=request.task,
|
|
|
brief=request.brief,
|
|
|
round=request.round,
|
|
|
attachments=request.attachments
|
|
|
)
|
|
|
generated_app = get_llm_helper().generate_app(app_request)
|
|
|
if not get_llm_helper().validate_generated_app(generated_app):
|
|
|
errors.append("Generated app failed validation; using fallback content")
|
|
|
fa = _fallback_generated_app(request.task)
|
|
|
class _Obj: pass
|
|
|
generated_app = _Obj()
|
|
|
generated_app.html_content = fa["html_content"]
|
|
|
generated_app.css_content = fa["css_content"]
|
|
|
generated_app.js_content = fa["js_content"]
|
|
|
generated_app.metadata = fa["metadata"]
|
|
|
except Exception as e:
|
|
|
errors.append(f"LLM generation error: {e}")
|
|
|
fa = _fallback_generated_app(request.task)
|
|
|
class _Obj: pass
|
|
|
generated_app = _Obj()
|
|
|
generated_app.html_content = fa["html_content"]
|
|
|
generated_app.css_content = fa["css_content"]
|
|
|
generated_app.js_content = fa["js_content"]
|
|
|
generated_app.metadata = fa["metadata"]
|
|
|
|
|
|
|
|
|
logger.info("Deploying to GitHub...")
|
|
|
repo_data = None
|
|
|
try:
|
|
|
|
|
|
extra_files = getattr(generated_app, 'extra_files', None)
|
|
|
if isinstance(generated_app.metadata, dict) and extra_files:
|
|
|
generated_app.metadata["_extra_files"] = extra_files
|
|
|
repo_data = get_github_helper().create_repo_and_deploy(
|
|
|
app_name=request.task,
|
|
|
html_content=generated_app.html_content,
|
|
|
css_content=generated_app.css_content,
|
|
|
js_content=generated_app.js_content,
|
|
|
metadata=generated_app.metadata,
|
|
|
is_revision=(request.round > 1)
|
|
|
)
|
|
|
except Exception as e:
|
|
|
errors.append(f"GitHub helper init/deploy error: {e}")
|
|
|
repo_data = {"success": False, "error": str(e)}
|
|
|
|
|
|
if not repo_data or not repo_data.get("success"):
|
|
|
if repo_data and repo_data.get("error"):
|
|
|
errors.append(f"GitHub deployment failed: {repo_data.get('error')}")
|
|
|
|
|
|
repo_data = _local_repo_deploy(
|
|
|
app_name=request.task,
|
|
|
html=generated_app.html_content,
|
|
|
css=generated_app.css_content,
|
|
|
js=generated_app.js_content,
|
|
|
metadata=generated_app.metadata,
|
|
|
)
|
|
|
|
|
|
|
|
|
logger.info("Notifying evaluation API...")
|
|
|
evaluation_result = {"success": False}
|
|
|
try:
|
|
|
if request.evaluation_url and isinstance(request.evaluation_url, str) and (
|
|
|
request.evaluation_url.startswith("http://") or request.evaluation_url.startswith("https://")
|
|
|
):
|
|
|
evaluation_result = await get_deploy_helper().notify_evaluation_api(
|
|
|
evaluation_url=request.evaluation_url,
|
|
|
email=request.email,
|
|
|
task=request.task,
|
|
|
round_num=request.round,
|
|
|
nonce=request.nonce,
|
|
|
repo_data=repo_data,
|
|
|
app_metadata=generated_app.metadata
|
|
|
)
|
|
|
except Exception as e:
|
|
|
errors.append(f"Evaluation notify failed: {e}")
|
|
|
|
|
|
response_data = {
|
|
|
"success": True,
|
|
|
"message": "Application generated and deployed successfully",
|
|
|
"deployment": {
|
|
|
"repo_name": repo_data.get("repo_name"),
|
|
|
"repo_url": repo_data.get("repo_url"),
|
|
|
"commit_sha": repo_data.get("commit_sha"),
|
|
|
"pages_url": repo_data.get("pages_url")
|
|
|
},
|
|
|
"evaluation_notification": {
|
|
|
"sent": evaluation_result.get("success", False),
|
|
|
"status_code": evaluation_result.get("status_code"),
|
|
|
"error": evaluation_result.get("error") if not evaluation_result.get("success") else None
|
|
|
},
|
|
|
"metadata": {
|
|
|
"round": request.round,
|
|
|
"nonce": request.nonce,
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
},
|
|
|
"errors": errors,
|
|
|
"fallback": repo_data.get("fallback", False)
|
|
|
}
|
|
|
|
|
|
if getattr(request, "return_code", False):
|
|
|
response_data["code"] = {
|
|
|
"html_content": generated_app.html_content,
|
|
|
"css_content": generated_app.css_content,
|
|
|
"js_content": generated_app.js_content,
|
|
|
"metadata": generated_app.metadata
|
|
|
}
|
|
|
|
|
|
logger.info(f"Request completed successfully for {request.email}")
|
|
|
logger.info(get_deploy_helper().format_deployment_summary(repo_data, generated_app.metadata))
|
|
|
|
|
|
return JSONResponse(status_code=status.HTTP_200_OK, content=response_data)
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Unexpected error processing request (responding 200 per policy): {e}")
|
|
|
fa = _fallback_generated_app("App")
|
|
|
repo_data = _local_repo_deploy("App", fa["html_content"], fa["css_content"], fa["js_content"], fa["metadata"])
|
|
|
return JSONResponse(
|
|
|
status_code=status.HTTP_200_OK,
|
|
|
content={
|
|
|
"success": True,
|
|
|
"message": "Application generated and deployed (fallback)",
|
|
|
"deployment": {
|
|
|
"repo_name": repo_data.get("repo_name"),
|
|
|
"repo_url": repo_data.get("repo_url"),
|
|
|
"commit_sha": repo_data.get("commit_sha"),
|
|
|
"pages_url": repo_data.get("pages_url")
|
|
|
},
|
|
|
"evaluation_notification": {"sent": False},
|
|
|
"metadata": {"round": 1, "nonce": "fallback", "timestamp": datetime.utcnow().isoformat()},
|
|
|
"errors": [str(e)],
|
|
|
"fallback": True
|
|
|
}
|
|
|
)
|
|
|
|
|
|
|
|
|
@app.post("/api/evaluate")
|
|
|
async def receive_evaluation(request: EvaluationRequest):
|
|
|
try:
|
|
|
logger.info(f"Received evaluation for {request.email}, round {request.round}")
|
|
|
logger.info(f"Evaluation data: {request.evaluation_data}")
|
|
|
return JSONResponse(
|
|
|
status_code=status.HTTP_200_OK,
|
|
|
content={
|
|
|
"success": True,
|
|
|
"message": "Evaluation received successfully",
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
}
|
|
|
)
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error processing evaluation: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
detail=f"Error processing evaluation: {str(e)}"
|
|
|
)
|
|
|
|
|
|
|
|
|
@app.exception_handler(404)
|
|
|
async def not_found_handler(request, exc):
|
|
|
return JSONResponse(status_code=404, content={"detail": "Endpoint not found"})
|
|
|
|
|
|
|
|
|
@app.exception_handler(500)
|
|
|
async def internal_error_handler(request, exc):
|
|
|
logger.error(f"Internal server error: {exc}")
|
|
|
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
import uvicorn
|
|
|
|
|
|
host = os.getenv("HOST", "0.0.0.0")
|
|
|
port = int(os.getenv("PORT", 8000))
|
|
|
debug = os.getenv("DEBUG", "false").lower() == "true"
|
|
|
|
|
|
uvicorn.run(
|
|
|
"main:app",
|
|
|
host=host,
|
|
|
port=port,
|
|
|
reload=debug,
|
|
|
log_level="info"
|
|
|
)
|
|
|
|
|
|
|
|
|
|