samrat-doi/LLM-Code-Deployment-Github
0
1from fastapi import FastAPI, Request, HTTPException2import requests3import os4import base645import time6# from dotenv import load_dotenv 7import traceback8import json9 10# load_dotenv()11 12GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")13AIPIPE_API_KEY = os.getenv("AIPIPE_API_KEY") 14GITHUB_USERNAME = os.getenv("GITHUB_USERNAME")15SECRET = os.getenv("SECRET")16 17if not all([GITHUB_TOKEN, AIPIPE_API_KEY, GITHUB_USERNAME, SECRET]):18 print("FATAL: Missing one or more essential environment variables.")19 20task_registry = {}21 22app = FastAPI()23 24def validate_secret(secret: str) -> bool:25 return secret == SECRET26 27def get_repo_info(repo_name: str):28 headers = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github+json"}29 response = requests.get(f"https://api.github.com/repos/{GITHUB_USERNAME}/{repo_name}", headers=headers)30 if response.status_code == 200:31 print(f"Repository {repo_name} already exists. Using existing one.")32 return response.json()33 return None34 35def create_github_repo(repo_name: str):36 print(f"Attempting to create or find repository: {repo_name}")37 repo_info = get_repo_info(repo_name)38 if repo_info:39 return repo_info40 41 payload = {"name": repo_name, "private": False, "auto_init": False, "license_template": "mit"}42 headers = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github+json"}43 response = requests.post("https://api.github.com/user/repos", headers=headers, json=payload)44 45 if response.status_code == 201:46 print(f"Successfully created new repository: {repo_name}")47 return response.json()48 if response.status_code == 422:49 repo_info = get_repo_info(repo_name)50 if repo_info:51 return repo_info52 else:53 raise Exception(f"Repo creation failed (422) AND subsequent GET failed: {response.content.decode('utf-8')}")54 55 raise Exception(f"Failed to create repository with status {response.status_code}: {response.content.decode('utf-8')}")56 57def enable_github_pages(repo_name: str):58 headers = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github+json"}59 payload = {"source": {"branch": "main", "path": "/"}}60 61 response = requests.post(f"https://api.github.com/repos/{GITHUB_USERNAME}/{repo_name}/pages", headers=headers, json=payload)62 63 if response.status_code not in [201, 409]:64 raise Exception(f"Failed to enable GitHub Pages (Status {response.status_code}): {response.content.decode('utf-8')}")65 66 print(f"GitHub Pages enabled for {repo_name}.")67 68 pages_response = requests.get(f"https://api.github.com/repos/{GITHUB_USERNAME}/{repo_name}/pages", headers=headers)69 if pages_response.status_code == 200:70 return pages_response.json().get('html_url')71 72 return f"https://{GITHUB_USERNAME}.github.io/{repo_name}/" 73 74def get_sha_of_latest_commit(repo_name: str, branch: str = "main") -> str:75 response = requests.get(76 f"https://api.github.com/repos/{GITHUB_USERNAME}/{repo_name}/commits/{branch}",77 headers={"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github+json"}78 )79 if response.status_code != 200:80 if response.status_code == 404:81 return None82 raise Exception(f"Failed to get latest commit SHA (Status {response.status_code}): {response.content.decode('utf-8')}")83 return response.json().get("sha")84 85def push_files_to_repo(repo_name: str, files: list, deployment_round: int):86 headers = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github+json"}87 88 for file in files:89 file_name = file.get("name")90 file_content = file.get("content")91 92 if isinstance(file_content, bytes):93 content_b64 = base64.b64encode(file_content).decode()94 elif isinstance(file_content, str):95 content_b64 = base64.b64encode(file_content.encode('utf-8')).decode()96 else:97 raise TypeError(f"File content for {file_name} must be bytes or str.")98 99 payload = {100 "message": f"[Round {deployment_round}] Update {file_name}",101 "content": content_b64,102 "branch": "main" 103 }104 105 file_info_response = requests.get(106 f"https://api.github.com/repos/{GITHUB_USERNAME}/{repo_name}/contents/{file_name}?ref=main",107 headers=headers108 )109 110 if file_info_response.status_code == 200:111 payload["sha"] = file_info_response.json().get("sha")112 113 response = requests.put(114 f"https://api.github.com/repos/{GITHUB_USERNAME}/{repo_name}/contents/{file_name}",115 headers=headers,116 json=payload117 )118 119 if response.status_code not in [200, 201]:120 raise Exception(f"Failed to push/update {file_name} (Status {response.status_code}): {response.content.decode('utf-8')}")121 122 print(f"Successfully pushed {file_name}. Commit SHA: {response.json().get('commit', {}).get('sha')}")123 124def generate_app_code(brief: str, attachments: list):125 system_instruction = (126 "You are an expert software engineer and code generator. "127 "Your only job is to generate a single, complete HTML file (index.html) based on the user's request. "128 "Do NOT include any external explanation, markdown delimiters (```), or comments in the final output. "129 "The output must be the raw, ready-to-use HTML code only."130 )131 user_prompt = (132 f"Generate a minimal, complete, and functional web application as a single index.html file "133 f"based on this brief: '{brief}'. "134 f"Context/Attachments provided: {attachments}. "135 "Ensure the HTML file is valid and complete."136 )137 138 combined_prompt = f"{system_instruction}\n\nUser Request: {user_prompt}"139 140 if not AIPIPE_API_KEY:141 raise Exception("AIPIPE_API_KEY environment variable is not set.")142 143 headers = {"Authorization": f"Bearer {AIPIPE_API_KEY}", "Content-Type": "application/json"}144 payload = {"model": "openai/gpt-4o-mini", "input": combined_prompt}145 146 aipipe_url = "https://aipipe.org/openrouter/v1/responses"147 print(f"Calling Ai pipe API at {aipipe_url}...")148 149 response = requests.post(aipipe_url, headers=headers, json=payload, timeout=60)150 151 if response.status_code != 200:152 try:153 error_details = response.json().get('error', {})154 error_message = error_details.get('message', f"Unknown error (Status {response.status_code})")155 except json.JSONDecodeError:156 error_message = response.content.decode('utf-8')157 raise Exception(f"Ai pipe API error: {error_message}")158 159 result = response.json()160 161 try:162 code = result['output'][0]['content'][0]['text'].strip()163 except (KeyError, IndexError, TypeError):164 print("ERROR: Could not parse AI response. Received structure:")165 print(json.dumps(result, indent=2))166 raise Exception("Failed to parse generated code from AI response.")167 168 return code169 170def generate_readme(data: dict, brief: str) -> str:171 task_name = data['task']172 return f"""# {task_name}173 174## Summary175 176This application was automatically generated based on the following brief:177> {brief}178 179## Setup180 181This is a static HTML application hosted on GitHub Pages. No setup is required. Simply visit the GitHub Pages URL to view the live application.182 183## Usage184 185Open the `index.html` file in your browser or visit the live GitHub Pages link.186 187## Code Explanation188 189The application consists of a single `index.html` file. It was generated by an AI model to fulfill the requirements of the project brief.190 191## License192 193This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.194"""195 196def generate_license() -> str:197 return """MIT License198 199Copyright (c) [2024] [Your Name or GitHub Username]200 201Permission is hereby granted, free of charge, to any person obtaining a copy202of this software and associated documentation files (the "Software"), to deal203in the Software without restriction, including without limitation the rights204to use, copy, modify, merge, publish, distribute, sublicense, and/or sell205copies of the Software, and to permit persons to whom the Software is206furnished to do so, subject to the following conditions:207 208The above copyright notice and this permission notice shall be included in all209copies or substantial portions of the Software.210 211THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR212IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,213FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE214AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER215LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,216OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE217SOFTWARE.218"""219 220def deploy_app(data, deployment_round: int):221 brief = data['brief']222 attachments = data.get('attachments', [])223 224 if deployment_round == 2:225 round2_data = data.get('round2', [])226 if round2_data and isinstance(round2_data, list):227 update_data = round2_data[0] 228 brief = update_data.get('brief', brief)229 if update_data.get('attachments'):230 attachments = update_data['attachments'] 231 232 task_name = data['task'].replace(' ', '-').lower() 233 repo_name = f"{task_name}_{data['nonce']}" 234 235 create_github_repo(repo_name)236 237 code_str = generate_app_code(brief, attachments)238 readme_content = generate_readme(data, brief)239 license_content = generate_license()240 241 files = [242 {"name": "index.html", "content": code_str},243 {"name": "README.md", "content": readme_content},244 {"name": "LICENSE", "content": license_content}245 ]246 247 push_files_to_repo(repo_name, files, deployment_round) 248 pages_url = enable_github_pages(repo_name)249 repo_url = f"https://github.com/{GITHUB_USERNAME}/{repo_name}"250 251 return repo_url, pages_url, repo_name252 253def post_evaluation(data, repo_url, sha, pages_url, round):254 payload = {255 "email": data["email"], "task": data["task"], "round": round, "nonce": data["nonce"],256 "repo_url": repo_url, "commit_sha": sha, "pages_url": pages_url257 }258 headers = {"Content-Type": "application/json"}259 260 evaluation_url = data.get("evaluation_url")261 if not evaluation_url:262 print("CRITICAL: evaluation_url is missing from the request data. Cannot post results.")263 return264 265 max_retries = 5266 retry_delay = 1267 268 for attempt in range(max_retries):269 print(f"Posting evaluation for Round {round} to {evaluation_url}... (Attempt {attempt + 1}/{max_retries})")270 try:271 response = requests.post(evaluation_url, headers=headers, json=payload, timeout=10)272 if response.status_code == 200:273 print("Successfully posted evaluation.")274 return275 print(f"Evaluation post failed with status {response.status_code}: {response.content.decode('utf-8')}")276 except requests.exceptions.RequestException as e:277 print(f"Evaluation post failed due to network error: {e}")278 279 if attempt < max_retries - 1:280 print(f"Retrying in {retry_delay} seconds...")281 time.sleep(retry_delay)282 retry_delay *= 2283 284 print("CRITICAL: All evaluation post attempts failed.")285 286def handle_round1(data):287 repo_url, pages_url, repo_name = deploy_app(data, deployment_round=1)288 sha = get_sha_of_latest_commit(repo_name)289 task_registry[data['nonce']] = repo_name290 post_evaluation(data, repo_url, sha, pages_url, round=1)291 292def handle_round2(data):293 nonce = data['nonce']294 known_repo = task_registry.get(nonce)295 296 if known_repo and get_repo_info(known_repo):297 repo_url, pages_url, repo_name = deploy_app(data, deployment_round=2)298 sha = get_sha_of_latest_commit(repo_name)299 post_evaluation(data, repo_url, sha, pages_url, round=2)300 else:301 repo_url, pages_url, repo_name = deploy_app(data, deployment_round=2)302 task_registry[nonce] = repo_name303 print(f"Repo not in registry or deleted. Used/Created repo {repo_name} and deployed.")304 sha = get_sha_of_latest_commit(repo_name)305 post_evaluation(data, repo_url, sha, pages_url, round=2)306 307@app.post("/handle_task")308async def handle_task(request: Request):309 try:310 data = await request.json()311 except json.JSONDecodeError:312 raise HTTPException(status_code=400, detail="Invalid JSON format.")313 314 if not validate_secret(data.get("secret")):315 raise HTTPException(status_code=401, detail="Invalid secret.")316 317 required_keys = ["round", "nonce", "task", "email", "evaluation_url"] 318 if not all(key in data for key in required_keys):319 missing = [key for key in required_keys if key not in data]320 raise HTTPException(status_code=400, detail=f"Missing required data keys: {', '.join(missing)}.")321 322 try:323 round_number = data.get("round")324 task_name = data['task'].replace(' ', '-').lower() 325 repo_name = f"{task_name}_{data['nonce']}" 326 327 if round_number == 1:328 handle_round1(data)329 return {"message": "Round 1 completed successfully", "repo_name": repo_name, "status": "success"}330 elif round_number == 2:331 handle_round2(data)332 return {"message": "Round 2 completed successfully", "repo_name": repo_name, "status": "success"}333 else:334 raise HTTPException(status_code=400, detail="Invalid round number. Must be 1 or 2.")335 336 except Exception as e:337 print(traceback.format_exc())338 raise HTTPException(status_code=500, detail=f"Deployment Error: {str(e)}")339 340if __name__ == "__main__":341 import uvicorn342 uvicorn.run(app, host="0.0.0.0", port=8000)