import httpx import json import re import logging from config.settings import get_settings logger = logging.getLogger(__name__) settings = get_settings() def _extract_json(text: str) -> dict: """ Bulletproof JSON extraction for Level 4-8 'Heavyweight' models. Supports markdown fences, meta-commentary, and nested structures. """ text = text.strip() # 1. Trial: Pure JSON try: return json.loads(text) except json.JSONDecodeError: pass # 2. Trial: Regex for ```json codes blocks fence_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text) if fence_match: try: return json.loads(fence_match.group(1).strip()) except json.JSONDecodeError: pass # 3. Trial: Brute-force first '{' to last '}' start = text.find('{') end = text.rfind('}') if start != -1 and end != -1 and end > start: json_str = text[start:end+1] try: return json.loads(json_str) except json.JSONDecodeError: pass raise ValueError(f"Could not extract valid JSON from AI response. Status: {text[:100]}...") def _sanitize_result(data: dict) -> dict: """ Ensures that 'issues' and 'suggestions' are always flat lists of strings. Handles 'Mistral Categorization' where the AI returns objects instead of arrays. """ for key in ["issues", "suggestions"]: val = data.get(key) if isinstance(val, dict): # Flatten dictionary: {"category": "issue"} -> ["category: issue"] new_list = [] for k, v in val.items(): if isinstance(v, list): new_list.extend([f"{k}: {item}" for item in v]) else: new_list.append(f"{k}: {v}") data[key] = new_list elif val and not isinstance(val, list): data[key] = [str(val)] elif not val: data[key] = [] return data # Smart Model Dictionary: Maps logical names to provider-specific model IDs # Ordered from Level 1 (Entry) to Level 8 (Flagship) MODEL_MAP = { "gemma-4-31b": { "openrouter": "google/gemma-4-31b-it:free" }, "llama-3.1": { "groq": "llama-3.1-8b-instant", "openrouter": "meta-llama/llama-3.1-8b-instruct:free" }, "qwen-2.5": { "openrouter": "qwen/qwen-2.5-7b-instruct:free", "huggingface": "Qwen/Qwen2.5-7B-Instruct" }, "nemotron-120b": { "openrouter": "nvidia/nemotron-3-super-120b-a12b:free" }, "minimax-2.5": { "openrouter": "minimax/minimax-m2.5:free" }, "mistral-large": { "openrouter": "mistralai/mistral-large-2407" }, "groq-70b": { "groq": "llama-3.3-70b-versatile", "openrouter": "meta-llama/llama-3.3-70b-instruct:free" }, "gemini-flash": { "gemini": "gemini-2.0-flash", "openrouter": "google/gemini-2.0-flash-001" } } async def analyze_with_groq(prompt: str, model_id: str = "llama-3.1-8b-instant") -> dict: """Backup AI — Groq with specified model.""" if not settings.groq_api_key: raise Exception("Groq API key not configured") # Dynamic timeout for large codebases timeout = 60.0 if len(prompt) > 5000 else 30.0 async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post( "https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {settings.groq_api_key}", "Content-Type": "application/json"}, json={ "model": model_id, "messages": [{"role": "user", "content": prompt}], "temperature": 0.1, "max_tokens": 2000 # Increased for large code analysis } ) if response.status_code != 200: raise Exception(f"Groq error {response.status_code}: {response.text[:200]}") result = response.json() text = result["choices"][0]["message"]["content"].strip() return _sanitize_result(_extract_json(text)) async def analyze_with_gemini(prompt: str, model_id: str = "gemini-2.0-flash") -> dict: """Primary AI — Gemini with specified model.""" if not settings.gemini_api_key: raise Exception("Gemini API key not configured") # Support large context in Gemini timeout = 60.0 if len(prompt) > 5000 else 30.0 async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post( f"https://generativelanguage.googleapis.com/v1beta/models/{model_id}:generateContent?key={settings.gemini_api_key}", json={ "contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"maxOutputTokens": 2000} # Increased } ) if response.status_code != 200: raise Exception(f"Gemini error {response.status_code}: {response.text[:200]}") result = response.json() candidates = result.get("candidates") if not candidates: raise Exception("Gemini returned no candidates") text = candidates[0]["content"]["parts"][0]["text"].strip() return _sanitize_result(_extract_json(text)) async def analyze_with_openrouter(prompt: str, model_id: str = None) -> dict: """Last Resort AI — OpenRouter with customizable model.""" if not settings.openrouter_api_key: raise Exception("OpenRouter API key not configured") # Use settings default if no specific model requested target_model = model_id or settings.openrouter_model # Support large context in OpenRouter timeout = 60.0 if len(prompt) > 5000 else 30.0 async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post( "https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {settings.openrouter_api_key}", "Content-Type": "application/json"}, json={ "model": target_model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 2000 # Increased } ) if response.status_code != 200: raise Exception(f"OpenRouter error {response.status_code}: {response.text[:200]}") result = response.json() if "choices" not in result: raise Exception(f"Unexpected OpenRouter response: {result}") text = result["choices"][0]["message"]["content"].strip() return _sanitize_result(_extract_json(text)) async def analyze_with_huggingface(prompt: str, model_id: str = None) -> dict: """Extra Backup — Hugging Face with specific model OR rotation.""" if not settings.huggingface_api_token: raise Exception("Hugging Face API token not configured") # If a specific model is requested, try ONLY that one # Otherwise, use the rotation logic models_to_try = [model_id] if model_id else settings.huggingface_free_models last_error = None for target_model in models_to_try: try: timeout = 60.0 if len(prompt) > 5000 else 30.0 async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post( f"https://api-inference.huggingface.co/models/{target_model}", headers={"Authorization": f"Bearer {settings.huggingface_api_token}"}, json={ "inputs": prompt, "parameters": {"max_new_tokens": 2000, "return_full_text": False} } ) if response.status_code != 200: raise Exception(f"HF Model {target_model} failed ({response.status_code})") result = response.json() text = result[0]["generated_text"].strip() if isinstance(result, list) else result.get("generated_text", "").strip() return _sanitize_result(_extract_json(text)) except Exception as e: logger.warning(f"Hugging Face model {target_model} failed: {e}") last_error = e if model_id: # Don't rotate if specific model was forced break continue raise Exception(f"Hugging Face attempt failed. Last error: {last_error}") async def route_analysis(prompt: str, model_choice: str = "auto") -> dict: """ Smart AI router with Zero-Failure 'Smart Recovery' logic. Tiered Chain: Gemma 4 -> Llama 3.1 -> Qwen 2.5 -> Nemotron 120B -> MiniMax 2.5 -> Mistral Large -> Groq 70B -> Gemini Flash """ # 1. Prepare Provider List providers = [] # Handle Specific Model Choice (With Recovery) if model_choice != "auto" and model_choice in MODEL_MAP: config = MODEL_MAP[model_choice] if "groq" in config: providers.append({"func": lambda p: analyze_with_groq(p, config["groq"]), "name": model_choice}) if "gemini" in config: providers.append({"func": lambda p: analyze_with_gemini(p, config["gemini"]), "name": model_choice}) if "openrouter" in config: providers.append({"func": lambda p: analyze_with_openrouter(p, config["openrouter"]), "name": model_choice}) if "huggingface" in config: providers.append({"func": lambda p: analyze_with_huggingface(p, config["huggingface"]), "name": model_choice}) # SMART RECOVERY: If the specific choice fails, pivot to the full AUTO chain providers.append({"func": lambda p: route_analysis(p, "auto"), "is_meta": True}) else: # Full Power Progression Chain (8 Levels) order = ["gemma-4-31b", "llama-3.1", "qwen-2.5", "nemotron-120b", "minimax-2.5", "mistral-large", "groq-70b", "gemini-flash"] for m_id in order: m_cfg = MODEL_MAP[m_id] # Primary provider logic for auto-chain if m_id == "llama-3.1": # Prioritize Groq's 8B for speed providers.append({"func": lambda p: analyze_with_groq(p, "llama-3.1-8b-instant"), "name": m_id}) elif m_id == "groq-70b": # Prioritize Groq's 70B providers.append({"func": lambda p: analyze_with_groq(p, "llama-3.3-70b-versatile"), "name": m_id}) elif m_id == "gemini-flash": providers.append({"func": lambda p: analyze_with_gemini(p), "name": m_id}) elif "openrouter" in m_cfg: providers.append({"func": lambda p: analyze_with_openrouter(p, m_cfg["openrouter"]), "name": m_id}) # Ultimate Last Resort providers.append({"func": lambda p: analyze_with_openrouter(p, "openrouter/free"), "name": "openrouter-free-fallback"}) # 2. Execute with Failover Logic last_error = None for i, p_item in enumerate(providers): try: p_name = p_item.get("name", "failover-chain") logger.info(f"Attempting {p_name} ({i+1}/{len(providers)})") result = await p_item["func"](prompt) # If it's a metadata-wrapped result from a nested call, return it if isinstance(result, dict) and "_actual_model" in result: return result # Attach actual model name to the result if isinstance(result, dict): result["_actual_model"] = p_name return result except Exception as e: logger.error(f"Provider {i+1} failed: {str(e)}") last_error = e continue raise Exception(f"Absolute failure in AI Hub. Last error: {last_error}")