Muskan02/llm-code-deployer
0
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"}