Team Ai
Apppublic

Muskan02/llm-code-deployer

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
main.py656 linesDownload Raw Back to root
1from fastapi import FastAPI, HTTPException2from starlette.responses import JSONResponse3from models import TaskRequest # Ensure models.py is available4from config import get_settings5import asyncio6import httpx # Used for making the HTTP notification call7import json # For parsing the structured JSON response from the LLM8import os # For configuration and file system operations9import base6410import re11import git  # For local Git operations12import time13import shutil14import stat # For robust cleanup on Windows15 16# Assuming this model is defined elsewhere17# --- Configuration and Setup ---18settings = get_settings()19 20# --- Helper Function for Security ---21def verify_secret(secret_from_request: str) -> bool:22    """Checks if the provided secret matches the expected student secret."""23    return secret_from_request == settings.APP_SECRET24 25# --- GITHUB CONSTANTS ---26GITHUB_API_BASE = "https://api.github.com"27# Pages URL is constructed dynamically using the username from settings28GITHUB_PAGES_BASE = f"https://{settings.GITHUB_USERNAME}.github.io"29# --------------------------30 31# LLM Configuration32GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent"33# NOTE: API key is left empty (or read from environment) as per instructions; 34# the execution environment is assumed to handle the required authentication.35OPEN_API_KEY = settings.OPEN_API_KEY 36# Initialize the FastAPI application37app = FastAPI(38    title="Automated Task Receiver & Processor",39    description="Endpoint for receiving task assignments and triggering AI code generation/deployment."40)41 42# Global storage for the last received task (for demonstration purposes)43received_task_data = {}44 45# --- REFACTORING: SPLIT deploy_to_github ---46 47async def setup_local_repo(local_path: str, repo_name: str, repo_url_auth: str, repo_url_http: str, round_index: int) -> git.Repo:48    """Handles creating the remote repo (R1) or cloning the existing one (R2+) into an EMPTY directory."""49    50    github_username = settings.GITHUB_USERNAME51    github_token = settings.GITHUB_TOKEN52    53    headers = {54        "Authorization": f"token {github_token}",55        "Accept": "application/vnd.github.v3+json",56        "X-GitHub-Api-Version": "2022-11-28"57    }58 59    async with httpx.AsyncClient(timeout=45) as client:60        try:61            # 1. CREATE or INITIALIZE REPO / CLONE EXISTING REPO62            if round_index == 1:63                print(f"   -> R1: Creating remote repository '{repo_name}'...")64                payload = {"name": repo_name, "private": False, "auto_init": True}65                response = await client.post(f"{GITHUB_API_BASE}/user/repos", json=payload, headers=headers)66                response.raise_for_status()67 68                # Initialize local git repo in the EMPTY path69                repo = git.Repo.init(local_path)70                repo.create_remote('origin', repo_url_auth)71                print("   -> R1: Local git repository initialized.")72            73            elif round_index >= 2:74                # Crucial part for Round 2: Cloning the existing work into the EMPTY local_path75                print(f"   -> R{round_index}: Cloning existing repository from {repo_url_http}...")76                # local_path is guaranteed to be empty due to the cleanup and directory creation in the main function77                repo = git.Repo.clone_from(repo_url_auth, local_path)78                print(f"   -> R{round_index}: Repository cloned and ready for update.")79            80            return repo81 82        except httpx.HTTPStatusError as e:83            print(f"--- [API ERROR] GitHub API call failed with status {e.response.status_code}: {e.response.text} ---")84            raise Exception("GitHub API call failed during repository setup.")85        except git.GitCommandError as e:86            print(f"--- [GIT ERROR] Failed to perform git operation: {e} ---")87            raise Exception("Git operation failed during repository setup.")88 89 90async def commit_and_publish(repo: git.Repo, task_id: str, round_index: int, repo_name: str) -> dict:91    """Handles adding, committing, pushing, and configuring GitHub Pages after files are saved."""92 93    github_username = settings.GITHUB_USERNAME94    github_token = settings.GITHUB_TOKEN95    96    headers = {97        "Authorization": f"token {github_token}",98        "Accept": "application/vnd.github.v3+json",99        "X-GitHub-Api-Version": "2022-11-28"100    }101    repo_url_http = f"https://github.com/{github_username}/{repo_name}"102 103    async with httpx.AsyncClient(timeout=45) as client:104        try:105            # 1. ADD, COMMIT, AND PUSH FILES106            # The new files (generated and attachments) are now in the local_path.107            repo.git.add(A=True)108            commit_message = f"Task {task_id} - Round {round_index}: LLM-generated app update/creation"109            repo.index.commit(commit_message)110            commit_sha = repo.head.object.hexsha111            print(f"   -> Files committed. SHA: {commit_sha}")112 113            # Ensure main branch consistency and push114            repo.git.branch('-M', 'main')115            print("   -> Branch renamed to 'main'.")116            repo.git.push('--set-upstream', 'origin', 'main', force=True)117            print("   -> Changes pushed to remote 'main' branch.")118 119            # Wait for GitHub to register the branch120            print("   -> Waiting 10 seconds for GitHub to register the main branch...")121            await asyncio.sleep(10)122 123            # 2. ENABLE GITHUB PAGES WITH ROBUST RETRIES124            print("   -> Enabling GitHub Pages with robust retries...")125            pages_api_url = f"{GITHUB_API_BASE}/repos/{github_username}/{repo_name}/pages"126            pages_payload = {"source": {"branch": "main", "path": "/"}}127            pages_max_retries = 5128            pages_base_delay = 3129 130            for retry_attempt in range(pages_max_retries):131                try:132                    pages_response = await client.get(pages_api_url, headers=headers)133                    is_configured = (pages_response.status_code == 200)134 135                    if is_configured:136                        print(f"   -> Pages exists. Updating configuration (Attempt {retry_attempt + 1}).")137                        (await client.put(pages_api_url, json=pages_payload, headers=headers)).raise_for_status()138                    else:139                        print(f"   -> Creating Pages configuration (Attempt {retry_attempt + 1}).")140                        (await client.post(pages_api_url, json=pages_payload, headers=headers)).raise_for_status()141 142                    print("   -> Pages configuration successful.")143                    break144                145                except httpx.HTTPStatusError as e:146                    if e.response.status_code == 422 and "main branch must exist" in e.response.text and retry_attempt < pages_max_retries - 1:147                        delay = pages_base_delay * (2 ** retry_attempt)148                        print(f"   -> [Timing Issue] Branch not recognized. Retrying in {delay} seconds...")149                        await asyncio.sleep(delay)150                    else:151                        raise152            else:153                raise Exception("Failed to configure GitHub Pages after multiple retries due to branch existence.")154 155            # 3. CONSTRUCT RETURN VALUES156            print("   -> Waiting 5 seconds for GitHub Pages deployment...")157            await asyncio.sleep(5)158 159            pages_url = f"{GITHUB_PAGES_BASE}/{repo_name}/"160 161            return {162                "repo_url": repo_url_http,163                "commit_sha": commit_sha,164                "pages_url": pages_url165            }166 167        except git.GitCommandError as e:168            print(f"--- [GIT ERROR] Failed to perform git operation: {e} ---")169            raise Exception("Git operation failed during deployment.")170        except httpx.HTTPStatusError as e:171            print(f"--- [API ERROR] GitHub API call failed with status {e.response.status_code}: {e.response.text} ---")172            raise Exception("GitHub API call failed during deployment.")173        except Exception as e:174            print(f"--- [CRITICAL ERROR] Deployment failed: {e} ---")175            raise176 177# --- REMOVED: Original deploy_to_github (replaced by setup_local_repo and commit_and_publish) ---178# The function name deploy_to_github is now DELETED.179 180        181def data_uri_to_gemini_part(data_uri: str) -> dict:182    """183    Extracts Base64 data and MIME type from a Data URI and formats it 184    as the 'inlineData' structure required for a Gemini API multimodal part.185    """186    if not data_uri or not data_uri.startswith("data:"):187        print("ERROR: Invalid Data URI provided.")188        return None189 190    try:191        # Extract MIME type and Base64 part using regex192        match = re.search(r"data:(?P<mime_type>[^;]+);base64,(?P<base64_data>.*)", data_uri, re.IGNORECASE)193        if not match:194            print("ERROR: Could not parse MIME type or base64 data from URI.")195            return None196 197        mime_type = match.group('mime_type')198        base64_data = match.group('base64_data')199 200        # Check if it's a known image type to ensure we only send images to the LLM201        if not mime_type.startswith("image/"):202            print(f"Skipping attachment with non-image MIME type: {mime_type}")203            return None204        205        return {206            "inlineData": {207                "data": base64_data,  # The Base64 string itself208                "mimeType": mime_type209            }210        }211    except Exception as e:212        print(f"ERROR creating Gemini Part from URI: {e}")213        return None214 215def is_image_data_uri(data_uri: str) -> bool:216    """Checks if the data URI refers to an image based on the MIME type."""217    if not data_uri.startswith("data:"):218        return False219    # Check for "image/" prefix in the MIME type part of the URI220    return re.search(r"data:image/[^;]+;base64,", data_uri, re.IGNORECASE) is not None221# --- Helper Functions for File System Operations ---222 223async def save_generated_files_locally(task_id: str, files: dict) -> str:224    """225    Saves the generated files (index.html, README.md, LICENSE) into a local 226    directory named after the task_id within the 'generated_tasks' folder.227    """228    base_dir = os.path.join(os.getcwd(), "generated_tasks")229    task_dir = os.path.join(base_dir, task_id)230    231    # Ensure the task-specific directory exists232    # NOTE: This directory is created earlier in the main orchestration function233    os.makedirs(task_dir, exist_ok=True)234    235    print(f"--- [LOCAL_SAVE] Saving files to: {task_dir} ---")236    237    # Write each file from the generated dictionary to the local file system238    for filename, content in files.items():239        file_path = os.path.join(task_dir, filename)240        try:241            # Write the content to the file. Assuming content is a string (text files).242            with open(file_path, "w", encoding="utf-8") as f:243                f.write(content)244            print(f"   -> Saved: {filename} (Size: {len(content)} bytes)")245        except Exception as e:246            print(f"   -> ERROR saving {filename}: {e}")247            # If saving fails, we treat this as a critical error for the task248            raise Exception(f"Failed to save file {filename} locally.")249 250    return task_dir251 252 253# --- Helper Functions for External Services ---254 255async def call_llm_for_code(prompt: str, task_id: str, image_parts: list) -> dict:256    """257    Calls the Gemini API to generate the web application code and structured258    metadata (README and LICENSE), now supporting image inputs.259    The response is strictly validated against a JSON schema.260    """261    print(f"--- [LLM_CALL] Attempting to generate code for Task: {task_id} using Gemini API ---")262      # --- Improve vague user prompts automatically ---263    normalized_prompt = prompt.lower().strip()264 265    # If the user gave a vague short task like "create a captcha solver..."266    # auto-extend it into a detailed instruction so Gemini knows what to do267    if "captcha solver" in normalized_prompt and "responsive" not in normalized_prompt:268        print("--- [LLM_CALL] Detected vague captcha solver prompt โ€” auto-expanding ---")269        prompt += (270            "\n\n"271            "Ensure the web app is a single, complete, fully responsive HTML file using Tailwind CSS. "272            "It must fetch an image from the query parameter '?url=https://.../image.png', display it, "273            "and perform OCR using Tesseract.js via CDN. "274            "If the URL parameter is missing, use the attached sample image by default. "275            "Show the recognized text and any errors clearly in the UI. "276            "Return output strictly as a JSON object with keys: 'index.html', 'README.md', and 'LICENSE'."277        )278    # Define system instruction for the model (UNCHANGED)279    system_prompt = (280        "You are an expert full-stack engineer and technical writer. Your task is to generate "281        "three files in a single structured JSON response: 'index.html', 'README.md', and 'LICENSE'. "282        "The 'index.html' must be a single, complete, fully responsive HTML file using Tailwind CSS "283        "for styling and must implement the requested application logic. The 'README.md' must be "284        "professional. The 'LICENSE' must contain the full text of the MIT license."285    )286    287    # Define the JSON response structure (UNCHANGED)288    response_schema = {289        "type": "OBJECT",290        "properties": {291            "index.html": {"type": "STRING", "description": "The complete, single-file HTML content with inline CSS and JS, using Tailwind."},292            "README.md": {"type": "STRING", "description": "The professional Markdown content for the project README."},293            "LICENSE": {"type": "STRING", "description": "The full text of the MIT license."}294        },295        "required": ["index.html", "README.md", "LICENSE"]296    }297 298    # --- CONSTRUCT THE CONTENTS FIELD ---299    contents = []300    301    if image_parts:302        # Combine image parts and the text prompt.303        all_parts = image_parts + [304            { "text": prompt } 305        ]306        contents.append({ "parts": all_parts })307    else:308        # If no images, use the original structure with only the text prompt309        contents.append({ "parts": [{ "text": prompt }] })310 311    # Construct the final API payload312    payload = {313        "contents": contents,  314        "systemInstruction": { "parts": [{ "text": system_prompt }] },315        "generationConfig": {316            "responseMimeType": "application/json",317            "responseSchema": response_schema318        }319    }320    321    # Use exponential backoff for the API call 322    max_retries = 3323    base_delay = 1324    325    for attempt in range(max_retries):326        try:327            # Construct the URL with the API key328            url = f"{GEMINI_API_URL}?key={OPEN_API_KEY}"329            async with httpx.AsyncClient(timeout=60) as client:330                response = await client.post(331                    url, 332                    json=payload,333                    headers={"Content-Type": "application/json"}334                )335                response.raise_for_status() # Raises an exception for 4xx/5xx status codes336                337                # Parse the response to get the structured JSON text338                result = response.json()339                340                # Extract the generated JSON string from the result341                json_text = result['candidates'][0]['content']['parts'][0]['text']342                343                # The LLM output is a JSON string, so we need to parse it into a Python dict344                generated_files = json.loads(json_text)345                346                print(f"--- [LLM_CALL] Successfully generated files on attempt {attempt + 1}. ---")347                return generated_files348 349        except httpx.HTTPStatusError as e:350            print(f"--- [LLM_CALL] HTTP Error on attempt {attempt + 1}: {e}. ---")351        except (httpx.RequestError, KeyError, json.JSONDecodeError) as e:352            # Catches network errors, missing structure in the result, or invalid JSON output353            print(f"--- [LLM_CALL] Processing Error on attempt {attempt + 1}: {e}. ---")354        355        if attempt < max_retries - 1:356            delay = base_delay * (2 ** attempt)357            print(f"--- [LLM_CALL] Retrying LLM call in {delay} seconds... ---")358            await asyncio.sleep(delay)359 360    # If all retries fail, we raise an exception which is caught downstream361    print("--- [LLM_CALL] Failed to generate code after multiple retries. ---")362    raise Exception("LLM Code Generation Failure")363 364 365async def notify_evaluation_server(366    evaluation_url: str, 367    email: str,          368    task_id: str, 369    round_index: int,    370    nonce: str, 371    repo_url: str,372    commit_sha: str,     373    pages_url: str       374) -> bool:375    """376    Calls the evaluation_url to notify the server that the code has been deployed.377    """378    payload = {379        "email": email,380        "task": task_id,381        "round": round_index,382        "nonce": nonce,383        "repo_url": repo_url,384        "commit_sha": commit_sha,385        "pages_url": pages_url  386    }387    388    max_retries = 3389    base_delay = 1390    391    print(f"--- [NOTIFICATION] Attempting to notify server at {evaluation_url} ---")392    393    for attempt in range(max_retries):394        try:395            async with httpx.AsyncClient(timeout=10) as client:396                response = await client.post(evaluation_url, json=payload)397                response.raise_for_status() # Raises an exception for 4xx/5xx status codes398                399                print(f"--- [NOTIFICATION] Successfully notified server. Response: {response.status_code} ---")400                return True401        except httpx.HTTPStatusError as e:402            print(f"--- [NOTIFICATION] HTTP Error on attempt {attempt + 1}: {e}. ---")403        except httpx.RequestError as e:404            print(f"--- [NOTIFICATION] Request Error on attempt {attempt + 1}: {e}. ---")405        406        if attempt < max_retries - 1:407            delay = base_delay * (2 ** attempt)408            print(f"--- [NOTIFICATION] Retrying in {delay} seconds... ---")409            await asyncio.sleep(delay)410            411    print(f"--- [NOTIFICATION] Failed to notify evaluation server after {max_retries} attempts. ---")412    return False413 414 415async def save_attachments_locally(task_dir: str, attachments: list) -> list:416    """417    Decodes and saves attachments (provided as Base64 Data URIs) into the task directory.418    Returns a list of saved filenames.419    """420    saved_files = []421    print(f"--- [ATTACHMENTS] Processing {len(attachments)} attachments for: {task_dir} ---")422    423    for attachment in attachments:424        filename = attachment.name 425        data_uri = attachment.url426        427        if not filename or not data_uri or not data_uri.startswith("data:"):428            print(f"    -> WARNING: Skipping invalid attachment entry: {filename}")429            continue430 431        # Use regex to extract the Base64 part of the URI (after base64,)432        match = re.search(r"base64,(.*)", data_uri, re.IGNORECASE)433        if not match:434            print(f"    -> ERROR: Could not find base64 data in URI for {filename}")435            continue436 437        base64_data = match.group(1)438        file_path = os.path.join(task_dir, filename)439 440        try:441            # Decode the base64 string442            file_bytes = base64.b64decode(base64_data)443            444            # Write the raw bytes to the file445            with open(file_path, "wb") as f:446                f.write(file_bytes)447            448            print(f"    -> Saved Attachment: {filename} (Size: {len(file_bytes)} bytes)")449            saved_files.append(filename)450            451        except Exception as e:452            print(f"    -> CRITICAL ERROR saving attachment {filename}: {e}")453            raise Exception(f"Failed to save attachment {filename} locally.")454 455    return saved_files456# --- Main Orchestration Logic ---457 458 459async def generate_files_and_deploy(task_data: TaskRequest):460    """461    The asynchronous background process that executes the main project workflow.462    It adapts the LLM prompt for multi-round tasks and fixes the cloning order.463    """464    task_id = task_data.task465    email = task_data.email         466    round_index = task_data.round 467    brief = task_data.brief468    evaluation_url = task_data.evaluation_url469    nonce = task_data.nonce470    attachments = task_data.attachments471    472    473    print(f"\n--- [PROCESS START] Starting background task for {task_id}, Round {round_index} ---")474    475    # Deployment configuration476    repo_name = task_id.replace(' ', '-').lower()477    github_username = settings.GITHUB_USERNAME478    github_token = settings.GITHUB_TOKEN479    repo_url_auth = f"https://{github_username}:{github_token}@github.com/{github_username}/{repo_name}.git"480    repo_url_http = f"https://github.com/{github_username}/{repo_name}"481    482    try:483        # 0. Setup local directory484        base_dir = os.path.join(os.getcwd(), "generated_tasks")485        local_path = os.path.join(base_dir, task_id)486 487        # --- ROBUST CLEANUP LOGIC ---488        # Crucial: Cleans up local directory before cloning or creating a new repo.489        if os.path.exists(local_path):490            print(f"--- [CLEANUP] Deleting existing local directory: {local_path} ---")491            492            def onerror(func, path, exc_info):493                """Error handler for shutil.rmtree to handle permission issues."""494                if exc_info[0] is PermissionError or 'WinError 5' in str(exc_info[1]):495                    os.chmod(path, stat.S_IWUSR)496                    func(path)497                else:498                    raise499 500            try:501                shutil.rmtree(local_path, onerror=onerror)502                print("--- [CLEANUP] Directory deleted successfully. ---")503            except Exception as e:504                print(f"!!! CRITICAL: Failed to clean up directory. Error: {e}")505                raise Exception(f"Failed to perform local cleanup: {e}")506        507        # Create the fresh, EMPTY directory (ready for clone or init)508        os.makedirs(local_path, exist_ok=True)509        # --- END ROBUST CLEANUP ---510        511        # 1. SETUP REPO (Clone or Init)512        # MUST run before any files are saved to local_path.513        print(f"--- [DEPLOYMENT] Setting up local Git repository for Round {round_index}... ---")514        repo = await setup_local_repo(515            local_path=local_path, 516            repo_name=repo_name, 517            repo_url_auth=repo_url_auth, 518            repo_url_http=repo_url_http, 519            round_index=round_index520        ) 521        522        # 2. Process Attachments for LLM Input523        image_parts = []524        attachment_list_for_llm_prompt = []525 526        for attachment in attachments:527            # Check for image parts for LLM input528            if is_image_data_uri(attachment.url):529                gemini_part = data_uri_to_gemini_part(attachment.url)530                if gemini_part:531                    image_parts.append(gemini_part)532            533            # List all attachment names for the prompt534            attachment_list_for_llm_prompt.append(attachment.name)535 536        print(f"--- [LLM_INPUT] Found {len(image_parts)} image(s) to pass to LLM. ---")537        538        attachment_list_str = ", ".join(attachment_list_for_llm_prompt)539        540        # 3. AI Code Generation - Adapt Prompt for Round 2541        542        # --- MODIFICATION START: Adapting the LLM Prompt ---543        if round_index > 1:544            # For Round 2+, tell the LLM it's modifying existing work545            llm_prompt = (546                f"UPDATE INSTRUCTION (ROUND {round_index}): You must modify the existing project files "547                f"(index.html, README.md, LICENSE) based on this new brief: '{brief}'. "548                "You must replace all content in 'index.html', 'README.md', and 'LICENSE' with new, complete versions "549                "that implement the requested modifications. The 'index.html' must remain a single, complete, "550                "fully responsive HTML file using Tailwind CSS."551            )552        else:553            # For Round 1, generate a new application554            llm_prompt = (555                f"Generate a complete, single-file HTML web application to achieve the following: {brief}. "556                "Ensure your code is fully responsive, and uses Tailwind CSS. "557                "Provide the code for the main web app, a README.md, and an MIT LICENSE."558            )559        560        # Add attachment context if files were provided, regardless of round.561        if attachment_list_str:562             llm_prompt += f"\nAdditional context: The following files are available in the project root: {attachment_list_str}. "563             llm_prompt += f"Ensure your code references these files correctly (if applicable)."564        # --- MODIFICATION END ---565        566        # Call LLM567        generated_files = await call_llm_for_code(llm_prompt, task_id, image_parts)568        569        # 4. Save Generated Code Locally570        # This overwrites the cloned files (index.html, README.md, LICENSE)571        await save_generated_files_locally(task_id, generated_files)572        573        # 5. Save Attachments Locally574        # This adds attachments (like data.csv) to the local directory575        # The attachment saving now happens *after* the clone/init, resolving the Round 2 error.576        await save_attachments_locally(local_path, attachments)577 578        # 6. COMMIT AND PUBLISH579        print(f"--- [DEPLOYMENT] Committing and Publishing task {task_id}, Round {round_index} to GitHub... ---")580        581        deployment_info = await commit_and_publish(582            repo=repo, 583            task_id=task_id,584            round_index=round_index,585            repo_name=repo_name586        )587        588        repo_url = deployment_info["repo_url"]589        commit_sha = deployment_info["commit_sha"]590        pages_url = deployment_info["pages_url"] 591        592        print(f"--- [DEPLOYMENT] Success! Repo: {repo_url}, Pages: {pages_url} ---")593        594        # 7. Notify the Evaluation Server595        await notify_evaluation_server(596            evaluation_url=evaluation_url, 597            email=email,598            task_id=task_id, 599            round_index=round_index,600            nonce=nonce, 601            repo_url=repo_url,602            commit_sha=commit_sha,603            pages_url=pages_url604        )605 606    except Exception as e:607        print(f"--- [CRITICAL FAILURE] Task {task_id} failed during processing: {e} ---")608        609    print(f"--- [PROCESS END] Background task for {task_id} completed. ---")610 611 612# --- FastAPI Endpoint ---613 614@app.post("/ready", status_code=200)615async def receive_task(task_data: TaskRequest):616    """617    API endpoint that receives the task payload. 618    It verifies the secret and starts the generation/deployment process in the background.619    """620    global received_task_data621    622    # 1. SECRET VERIFICATION (CRITICAL PROJECT REQUIREMENT)623    if not verify_secret(task_data.secret):624        print(f"--- FAILED SECRET VERIFICATION for task {task_data.task} ---")625        raise HTTPException(626            status_code=401, 627            detail="Unauthorized: Secret does not match configured student secret."628        )629 630    # Store data and print initial confirmation631    received_task_data = task_data.dict()632    633    print("--- TASK RECEIVED SUCCESSFULLY ---")634    print(f"Task ID: {received_task_data['task']}, Round: {received_task_data['round']}")635    636    # Start the processing function in the background 637    asyncio.create_task(generate_files_and_deploy(task_data))638 639    # Respond immediately with 200 OK to the evaluation server640    return JSONResponse(641        status_code=200,642        content={"status": "ready", "message": f"Task {task_data.task} received and processing started."}643    )644 645@app.get("/")646async def root():647    return {"message": "Task Receiver Service is running. Post to /ready to submit a task."}648 649@app.get("/status")650async def get_status():651    global received_task_data652    if received_task_data:653        # Note: This status only shows the last received request, not the live status of the background task.654        return {"last_received_task": received_task_data}655    else:656        return {"message": "Awaiting first task submission to /ready"}