Spaces:
Sleeping
Sleeping
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = [ | |
| # "fastapi[standard]", | |
| # "uvicorn", | |
| # "requests", | |
| # ] | |
| # /// | |
| import os | |
| import requests | |
| import base64 | |
| from fastapi import FastAPI | |
| GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") | |
| def validate_secret(secret: str) -> bool: | |
| return secret == os.getenv("SECRET_KEY") | |
| def round1(data): | |
| if data.get("attachments", []): | |
| attachfiles = parse_attachments(data) | |
| else: | |
| attachfiles = None | |
| if data.get("checks", []): | |
| checks = data["checks"] | |
| else: | |
| checks = None | |
| if attachfiles: | |
| attach_text = "\n\n".join([f"{f['name']}\n{f['content']}" for f in attachfiles]) | |
| else: | |
| attach_text = "No attachments provided" | |
| if checks: | |
| checks_text = "\n".join(checks) | |
| else: | |
| checks_text = "No checks provided" | |
| prompt = f""" | |
| You are to create a simple web app based on this brief: | |
| {data['brief']} | |
| Make sure it works when deployed to GitHub Pages. | |
| Use HTML + JS + minimal CSS. | |
| include only code files and no explanations or markdown formatting. | |
| The app should be contained in a single HTML file named index.html. | |
| The following are the attachments provided, you can use them as needed: | |
| {attach_text} ------------- | |
| it must pass the following checks: | |
| {checks_text} | |
| """ | |
| files = write_code_with_llm(prompt) | |
| repo_name = f"{data['task']}-{data['nonce']}" | |
| create_repo(repo_name) | |
| enable_pages(repo_name) | |
| # Encode files to base64 before pushing | |
| commit_sha = push_files_to_pages(repo_name, files, 1) | |
| # Send POST back to evaluation URL | |
| post_evaluation(data, repo_name, commit_sha) | |
| def parse_attachments(data: dict)-> list[dict]: | |
| """ | |
| Parse attachments array and decode data: URIs into file objects: | |
| returns list of {"name": ..., "content": "..."} | |
| """ | |
| def safe_b64decode(b64_str: str): | |
| # Strip whitespace/newlines | |
| b64_str = b64_str.strip() | |
| # Add padding if missing | |
| missing_padding = len(b64_str) % 4 | |
| if missing_padding: | |
| b64_str += "=" * (4 - missing_padding) | |
| return base64.b64decode(b64_str) | |
| files = [] | |
| for att in data.get("attachments", []): | |
| name = att.get("name") | |
| url = att.get("url", "") | |
| if not name or not url: | |
| continue | |
| if url.startswith("data:"): | |
| try: | |
| header, b64 = url.split(",", 1) | |
| decoded = safe_b64decode(b64) | |
| try: | |
| text = decoded.decode("utf-8") | |
| files.append({"name": name, "content": text, "binary": False}) | |
| except UnicodeDecodeError: | |
| b64_text = base64.b64encode(decoded).decode("utf-8") | |
| files.append({"name": name, "content": b64_text, "binary": True}) | |
| except Exception as e: | |
| raise ValueError(f"Invalid data URI for attachment {name}: {e}") | |
| else: | |
| # non-data URIs are not fetched for security/simplicity; treat as placeholder | |
| raise ValueError("Only data: attachments are supported by this service.") | |
| return files | |
| def round2(data): | |
| repo_name = f"{data['task']}-{data['nonce']}" | |
| for i, subround in enumerate(data.get("round2", []), start=1): | |
| print(f"--- Starting Round 2.{i} ---") | |
| if subround.get("attachments", []): | |
| attachfiles = parse_attachments(subround) | |
| else: | |
| attachfiles = [] | |
| checks = subround.get("checks", []) | |
| attach_text = "\n\n".join([f"{f['name']}\n{f['content']}" for f in attachfiles]) if attachfiles else "No new attachments provided" | |
| checks_text = "\n".join(checks) if checks else "No checks provided" | |
| prompt = f""" | |
| You are to modify the existing web app (index.html) based on this new brief: | |
| {subround['brief']} | |
| The app already exists in the GitHub repository: {repo_name} | |
| You must update the existing index.html to fulfill the new requirements. | |
| Keep using HTML + JS + minimal CSS. | |
| Maintain compatibility with GitHub Pages. | |
| Attachments (if any) that you can use: | |
| {attach_text} | |
| It must pass the following checks: | |
| {checks_text} | |
| Include only code (no markdown or explanation). | |
| The updated app must remain inside a single file: index.html. | |
| """ | |
| # Generate the updated HTML with LLM | |
| files = write_code_with_llm(prompt) | |
| # Encode files to base64 before pushing | |
| # Push changes to GitHub (Round 2 update mode) | |
| commit_sha = push_files_to_pages(repo_name, files, 2) | |
| # Post evaluation for this sub-round | |
| post_evaluation(data, repo_name, commit_sha) | |
| print(f"✅ Completed Round 2.{i} | Commit SHA: {commit_sha}") | |
| def create_repo(name: str): | |
| payload={"name":name, | |
| "private": False, | |
| "auto_init": False, | |
| "license_template": "mit"} | |
| header={"Authorization": f"Bearer {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github.v3+json"} | |
| response = requests.post( | |
| "https://api.github.com/user/repos", | |
| headers=header, | |
| json=payload | |
| ) | |
| if response.status_code != 201: | |
| return Exception(f"Failed to create repo : {response.status_code}, {response.text}") | |
| else: | |
| return response.json() | |
| def enable_pages(repo_name: str): | |
| headers={ | |
| "Authorization": f"Bearer {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| payload={ | |
| "source":{ | |
| "branch":"main", | |
| "path":"/" | |
| }, | |
| "build_type":"legacy"} | |
| response = requests.post( | |
| f"https://api.github.com/repos/23f2000524/{repo_name}/pages", | |
| headers=headers, | |
| json=payload | |
| ) | |
| if response.status_code != 201: | |
| return Exception(f"Failed to enable pages : {response.status_code}, {response.text}") | |
| else: | |
| return response.json() | |
| def get_sha_of_latest_commit(repo_name: str, branch: str="main") -> str: | |
| headers={ | |
| "Authorization": f"Bearer {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| response = requests.get( | |
| f"https://api.github.com/repos/23f2000524/{repo_name}/git/refs/heads/{branch}", | |
| headers=headers | |
| ) | |
| if response.status_code != 200: | |
| return Exception(f"Failed to get file sha : {response.status_code}, {response.text}") | |
| else: | |
| return response.json()["object"]["sha"] | |
| def get_file_sha(repo_name: str, file_path: str, branch: str = "main") -> str: | |
| headers = { | |
| "Authorization": f"Bearer {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| resp = requests.get( | |
| f"https://api.github.com/repos/23f2000524/{repo_name}/contents/{file_path}?ref={branch}", | |
| headers=headers | |
| ) | |
| if resp.status_code != 200: | |
| raise Exception(f"Failed to get file sha for {file_path} : {resp.status_code}, {resp.text}") | |
| return resp.json()["sha"] | |
| def push_files_to_pages(repo_name: str,files:list[dict],round:int): | |
| if round == 2: | |
| latest_sha = get_sha_of_latest_commit(repo_name) | |
| else: | |
| latest_sha = None | |
| commit_sha = None | |
| for file in files: | |
| file_name=file.get("name") | |
| file_content=file.get("content") | |
| binary=file.get("binary", False) | |
| if not isinstance(file_content, bytes): | |
| if binary: | |
| # base64 string -> bytes | |
| file_content = base64.b64decode(file_content) | |
| else: | |
| file_content = file_content.encode("utf-8") | |
| b64_content = base64.b64encode(file_content).decode("utf-8") | |
| headers={ | |
| "Authorization": f"Bearer {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| payload={ | |
| "message": f"Update {file_name}" if latest_sha else f"Add {file_name}", | |
| "content": b64_content | |
| } | |
| sha = get_file_sha(repo_name, file_name) if round == 2 else None | |
| if sha : | |
| payload["sha"] = sha | |
| response = requests.put( | |
| f"https://api.github.com/repos/23f2000524/{repo_name}/contents/{file_name}", | |
| headers=headers, | |
| json=payload | |
| ) | |
| if response.status_code not in [200,201]: | |
| raise Exception(f"Failed to push file {file_name} : {response.status_code}, {response.text}") | |
| commit_sha = response.json()["commit"]["sha"] | |
| return commit_sha | |
| def write_code_with_llm(prompt: str): | |
| API_URL = "https://aipipe.org/openai/v1/chat/completions" | |
| API_KEY = os.getenv("AIAPI_KEY") | |
| headers = { | |
| "Content-Type": "application/json", | |
| "Authorization": f"Bearer {API_KEY}" | |
| } | |
| data = { | |
| "model": "gpt-4o", | |
| "messages": [{"role": "user", "content": prompt}], | |
| "max_tokens": 1000, | |
| "temperature": 0.7 | |
| } | |
| resp = requests.post(API_URL, headers=headers, json=data) | |
| if resp.status_code != 200: | |
| raise Exception(f"LLM API error: {resp.status_code} - {resp.text}") | |
| code = resp.json()["choices"][0]["message"]["content"].strip() | |
| return [ | |
| {"name": "index.html", "content": code}, | |
| {"name": "README.md", "content": f"# Generated App\n\n## Latest Code Generated based on :\n{prompt}\n\n---\n\n## Generated Code\n\n{code}"} ] | |
| def post_evaluation(data, repo_name, commit_sha): | |
| payload = { | |
| "email": data["email"], | |
| "task": data["task"], | |
| "round": data["round"], | |
| "nonce": data["nonce"], | |
| "repo_url": f"https://github.com/23f2000524/{repo_name}", | |
| "commit_sha": commit_sha, # optional: fetch via API | |
| "pages_url": f"https://23f2000524.github.io/{repo_name}/" | |
| } | |
| print(payload) | |
| headers = {"Content-Type": "application/json"} | |
| r = requests.post(data["evaluation_url"], headers=headers, json=payload) | |
| if r.status_code != 200: | |
| raise Exception(f"Eval post failed: {r.status_code} - {r.text}") | |
| return True | |
| app = FastAPI() | |
| def handle_task(data: dict): | |
| if not validate_secret(data.get("secret", "")): | |
| return {"error": "Incorrect secret"} | |
| else: | |
| if data.get("round") == 1: | |
| round1(data) | |
| return {"message": "Round 1 started"} | |
| elif data.get("round") == 2: | |
| round2(data) | |
| return {"message": "Round 2 started"} | |
| else: | |
| return {"error": "Invalid round"} | |
| print(data) | |
| return {"message": "Task recieved", "data": data} | |
| def root(): | |
| return {"message": "API is running. Use /handle_task for POST requests."} |