import json import os import re import time from bs4 import BeautifulSoup, Comment import requests # ───────────────────────────────────────────────────────────────────────────── # Model fallback chain (primary → fallback1 → fallback2) # ───────────────────────────────────────────────────────────────────────────── _FREE_MODEL_CHAIN = [ "google/gemma-4-31b-it:free", "qwen/qwen3-next-80b-a3b-instruct:free", "qwen/qwen3-coder:free", "meta-llama/llama-3.3-8b-instruct:free" ] # Files larger than this get the old tag-by-tag approach to stay within context limits _MAX_WHOLE_DOC_CHARS = 12_000 def _openrouter_headers(): key = os.environ.get("OPENROUTER_API_KEY", "") return { "Authorization": f"Bearer {key}", "Content-Type": "application/json", "HTTP-Referer": "https://webcraft.com", "X-Title": "WebCraft AI Backend", } def _call_openrouter(model, messages, timeout=90, label=""): """Make a single OpenRouter API call. Returns response or None.""" try: return requests.post( url="https://openrouter.ai/api/v1/chat/completions", headers=_openrouter_headers(), data=json.dumps({"model": model, "messages": messages}), timeout=timeout, ) except requests.exceptions.Timeout: print(f" OpenRouter Timeout ({label or model})") return None except Exception as e: print(f" OpenRouter Request Error ({label or model}): {e}") def _call_huggingface(messages, timeout=90): """Make a single Hugging Face Inference API call as an ultimate fallback.""" hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN", "") if not hf_token: print(" HF_TOKEN not found for Hugging Face failover.") return None model = "microsoft/Phi-3.5-mini-instruct" print(f" [Ultimate Failover] Trying Hugging Face Inference: {model}") try: from huggingface_hub import InferenceClient client = InferenceClient(api_key=hf_token, timeout=30) reply = client.chat_completion(model=model, messages=messages, max_tokens=2000) return reply.choices[0].message.content.strip() except Exception as e: print(f" Hugging Face Request Error: {e}") return None def _try_models(messages, label=""): """ Try each model in the fallback chain. Returns the raw text content on success, or raises RuntimeError if all fail. """ for global_attempt, model in enumerate(_FREE_MODEL_CHAIN): for retry in range(3): if retry > 0: print(f" [Retry {retry}/3] Waiting 5s before retrying {model}...") time.sleep(5) elif global_attempt > 0 and retry == 0: print(f" Moving to fallback {global_attempt+1}/{len(_FREE_MODEL_CHAIN)}: {model}") time.sleep(2) resp = _call_openrouter(model, messages, label=label or f"attempt {global_attempt+1}") if resp is None: continue if resp.status_code == 200: try: return resp.json()["choices"][0]["message"]["content"].strip() except (KeyError, Exception) as e: print(f" Parse error ({model}): {e} — trying next") continue else: tag = "Primary" if global_attempt == 0 else "Fallback" print(f" OpenRouter {tag} Error ({model}): Code {resp.status_code}") try: print(f" Reason: {resp.json().get('error', {}).get('message', resp.text)}") except: print(f" Reason: {resp.text}") # ========================== # ULTIMATE FAILOVER: Hugging Face # ========================== for retry in range(3): if retry > 0: print(f" [Retry {retry}/3] Waiting 5s before retrying Hugging Face...") time.sleep(5) hf_resp_text = _call_huggingface(messages) if hf_resp_text: return hf_resp_text raise RuntimeError("AI_EXHAUSTED") # ───────────────────────────────────────────────────────────────────────────── # STRATEGY A — Whole-document annotation (BEST quality, used by default) # The AI sees the full HTML at once → context-aware, page-specific comments. # ───────────────────────────────────────────────────────────────────────────── _WHOLE_DOC_SYSTEM = """\ You are an expert Web Development Instructor. Your job is to add educational \ HTML comments to the code a student wrote. RULES 1. Return the COMPLETE HTML file with your comments inserted — nothing else. 2. Add a structured comment block DIRECTLY ABOVE every meaningful structural \ element:
,