Spaces:
Sleeping
Sleeping
Download app/main.py from adrija93/TDS-Project1-API: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/spaces/adrija93/TDS-Project1-API/resolve/main/app/main.py
- Command line
-
hf download hf://spaces/adrija93/TDS-Project1-API/app/main.py
-
curl -L -o main.py https://huggingface.co/spaces/adrija93/TDS-Project1-API/resolve/main/app/main.py
10.8 kB
| import time | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, HTTPException, BackgroundTasks | |
| from fastapi.responses import JSONResponse | |
| from app.models import TaskRequest, TaskResponse, ErrorResponse | |
| from app.config import get_settings | |
| from app.services.aipipe_client import AIpipeClient | |
| from app.services.github_service import GitHubService | |
| from app.services.pages import wait_for_200 | |
| from app.services.notify import NotificationService | |
| from app.services.storage import init_db, get_default_db_path, upsert_task, get_task | |
| from typing import Dict | |
| async def lifespan(app: FastAPI): | |
| # Startup: Initialize SQLite DB | |
| db_path = get_default_db_path() | |
| init_db(db_path) | |
| yield | |
| # Shutdown: nothing to cleanup currently | |
| app = FastAPI(title="LLM Code Deployment Builder", lifespan=lifespan) | |
| async def homepage(): | |
| return {"status": "Service is running"} | |
| async def healthz(): | |
| return {"status": "ok"} | |
| async def process_task_async(app: FastAPI, req: TaskRequest, settings) -> None: | |
| """Process the task asynchronously and notify evaluation URL.""" | |
| notification_service = NotificationService() | |
| try: | |
| start_time = time.time() | |
| # Initialize task state | |
| if not hasattr(app.state, "tasks"): | |
| app.state.tasks = {} | |
| if not hasattr(app.state, "tasks_by_task"): | |
| app.state.tasks_by_task = {} | |
| app.state.tasks[req.nonce] = {"status": "processing"} | |
| # Build attachments meta | |
| attachments_meta = [{"name": a.name, "url": a.url} for a in req.attachments] | |
| ai = AIpipeClient(settings.aipipe_model) | |
| # ROUND 2: revision flow keyed by task name | |
| if req.round == 2: | |
| db_path = get_default_db_path() | |
| print(f"[Round2][{req.task}] Starting revision for task. Nonce={req.nonce}") | |
| # Prefer DB, fallback to memory if present | |
| prior = get_task(db_path, req.task) or app.state.tasks_by_task.get(req.task) | |
| # If not found, fallback to GitHub lookup by task-derived name | |
| if (not prior) or (not prior.get("repo_url")): | |
| gh = GitHubService(settings.github_token, settings.github_username, settings.github_email) | |
| repo_url_fallback = gh.find_repo_by_task(req.task) | |
| if repo_url_fallback: | |
| prior = prior or {} | |
| prior.update({ | |
| "repo_url": repo_url_fallback, | |
| "pages_url": gh.get_pages_url(repo_url_fallback), | |
| }) | |
| if not prior or not prior.get("repo_url"): | |
| raise RuntimeError("No prior repository found for this task. Run round 1 first.") | |
| gh = GitHubService(settings.github_token, settings.github_username, settings.github_email) | |
| print(f"[Round2][{req.task}] Found repo URL: {prior['repo_url']}") | |
| repo_full_name = gh._repo_full_name_from_url(prior["repo_url"]) # owner/repo | |
| print(f"[Round2][{req.task}] Repo full name: {repo_full_name}") | |
| print(f"[Round2][{req.task}] Fetching current files from repo...") | |
| current_files = gh.get_text_files(repo_full_name, [ | |
| "README.md", "index.html", "style.css", "script.js" | |
| ]) | |
| print(f"[Round2][{req.task}] Retrieved files: {list(current_files.keys())}") | |
| print(f"[Round2][{req.task}] Requesting minimal changes from LLM...") | |
| changed_files: Dict[str, str] = await ai.generate_revision_files( | |
| req.brief, req.checks, attachments_meta, current_files | |
| ) | |
| if not changed_files: | |
| base = current_files.get("README.md", "# Project\n\n") | |
| changed_files = {"README.md": base + "\n\nRound 2: No code changes required.\n"} | |
| print(f"[Round2][{req.task}] LLM returned no changes; applying README bump to trigger deploy.") | |
| else: | |
| print(f"[Round2][{req.task}] Applying {len(changed_files)} changed file(s): {list(changed_files.keys())}") | |
| print(f"[Round2][{req.task}] Updating repository with changes...") | |
| commit_sha = gh.update_files_in_repo( | |
| repo_full_name, changed_files, commit_message_prefix="feat(revision)" | |
| ) | |
| print(f"[Round2][{req.task}] Update complete. Commit SHA: {commit_sha}") | |
| repo_url = prior["repo_url"] | |
| repo_name = repo_url.rstrip("/").split("/")[-1] | |
| pages_url = gh.get_pages_url(repo_url, repo_name) | |
| # Use internal defaults (no env needed) for Pages readiness | |
| print(f"[Round2][{req.task}] Waiting for GitHub Pages (using internal defaults)...") | |
| ok = await wait_for_200(pages_url) | |
| if not ok: | |
| raise RuntimeError("GitHub Pages did not become reachable within the timeout after round 2 updates") | |
| # Wait for pages to update; use non-blocking sleep to avoid blocking the event loop | |
| import asyncio | |
| await asyncio.sleep(60) | |
| print(f"[Round2][{req.task}] Pages check successful: {pages_url}") | |
| result = { | |
| "email": req.email, | |
| "task": req.task, | |
| "round": req.round, | |
| "nonce": req.nonce, | |
| "repo_url": repo_url, | |
| "commit_sha": commit_sha, | |
| "pages_url": pages_url, | |
| } | |
| print(f"[Round2][{req.task}] Notifying evaluation URL...") | |
| await notification_service.notify_evaluation(req.evaluation_url, result) | |
| print(f"[Round2][{req.task}] Round 2 completed successfully.") | |
| app.state.tasks[req.nonce] = {"status": "completed", "result": result} | |
| app.state.tasks_by_task[req.task] = { | |
| "repo_url": repo_url, | |
| "pages_url": pages_url, | |
| "commit_sha": commit_sha, | |
| } | |
| # Persist to DB | |
| upsert_task(db_path, req.task, repo_url, pages_url, commit_sha) | |
| return | |
| # ROUND 1: initial build | |
| print(f"[Round1][{req.task}] Starting initial build (Nonce={req.nonce})") | |
| print(f"[Round1][{req.task}] Using model: {ai.model}") | |
| files: Dict[str, str] = await ai.generate_app_files(req.brief, req.checks, attachments_meta) | |
| # Check time limit (10 minutes = 600 seconds) | |
| if time.time() - start_time > 540: | |
| raise TimeoutError("Task processing exceeded time limit") | |
| if not (settings.github_token and settings.github_username and settings.github_email): | |
| raise HTTPException(status_code=400, detail="GitHub config missing") | |
| gh = GitHubService(settings.github_token, settings.github_username, settings.github_email) | |
| repo_info = gh.create_repo_with_files(req.task, files, req.nonce) | |
| repo_url = repo_info["repo_url"] | |
| repo_name = repo_url.rstrip("/").split("/")[-1] | |
| pages_url = gh.get_pages_url(repo_url, repo_name) | |
| print(f"[Round1][{req.task}] Repository created: {repo_url}") | |
| ok = await wait_for_200(pages_url) | |
| if not ok: | |
| raise RuntimeError("GitHub Pages did not become reachable within the timeout after initial deployment") | |
| commit_sha = gh.get_latest_commit_sha(f"{settings.github_username}/{repo_name}") | |
| result = { | |
| "email": req.email, | |
| "task": req.task, | |
| "round": req.round, | |
| "nonce": req.nonce, | |
| "repo_url": repo_url, | |
| "commit_sha": commit_sha, | |
| "pages_url": pages_url, | |
| } | |
| await notification_service.notify_evaluation(req.evaluation_url, result) | |
| print(f"[Round1][{req.task}] Completed successfully.") | |
| app.state.tasks[req.nonce] = {"status": "completed", "result": result} | |
| app.state.tasks_by_task[req.task] = { | |
| "repo_url": repo_url, | |
| "pages_url": pages_url, | |
| "commit_sha": commit_sha, | |
| } | |
| # Persist to DB for future round 2 | |
| db_path = get_default_db_path() | |
| upsert_task(db_path, req.task, repo_url, pages_url, commit_sha) | |
| except Exception as e: | |
| print(f"[Error][{getattr(req, 'task', 'unknown')}] {e}") | |
| error_result = { | |
| "email": req.email, | |
| "task": req.task, | |
| "round": req.round, | |
| "status": "error", | |
| "error": str(e), | |
| } | |
| try: | |
| evaluation_url = str(req.evaluation_url) | |
| await notification_service.notify_evaluation(evaluation_url, error_result) | |
| except Exception as notify_err: | |
| print(f"Failed to notify evaluation URL: {notify_err}") | |
| if hasattr(app.state, "tasks"): | |
| app.state.tasks[req.nonce] = {"status": "failed", "error": str(e)} | |
| finally: | |
| await notification_service.close() | |
| async def handle_task(req: TaskRequest, background_tasks: BackgroundTasks): | |
| settings = get_settings() | |
| # Secret check | |
| if not settings.app_secret or req.secret != settings.app_secret: | |
| raise HTTPException(status_code=400, detail="Invalid secret") | |
| # For round 2, ensure the task exists from round 1 BEFORE returning 200 | |
| if req.round == 2: | |
| db_path = get_default_db_path() | |
| # Initialize in-memory mapping if absent | |
| if not hasattr(app.state, "tasks_by_task"): | |
| app.state.tasks_by_task = {} | |
| prior = get_task(db_path, req.task) or app.state.tasks_by_task.get(req.task) | |
| if not prior or not prior.get("repo_url"): | |
| # Fallback to GitHub lookup by task-derived name before rejecting | |
| gh = GitHubService(settings.github_token, settings.github_username, settings.github_email) | |
| repo_url_fallback = gh.find_repo_by_task(req.task) | |
| if repo_url_fallback: | |
| # Stash minimally so background can proceed | |
| app.state.tasks_by_task[req.task] = {"repo_url": repo_url_fallback} | |
| else: | |
| raise HTTPException(status_code=400, detail="No prior repository found for this task. Run round 1 first.") | |
| # Initialize task state storage if needed | |
| if not hasattr(app.state, "tasks"): | |
| app.state.tasks = {} | |
| # Add task to background processing | |
| background_tasks.add_task(process_task_async, app, req, settings) | |
| # Immediate response with task accepted and required fields | |
| return TaskResponse( | |
| status="accepted", | |
| task=req.task, | |
| round=req.round, | |
| message="Task is being processed" | |
| ) | |