# core/llm_client.py import asyncio import httpx from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from agents.registry import PROVIDERS, AGENT_REGISTRY class LLMClient: def __init__(self): self.semaphore = asyncio.Semaphore(6) # máximo 6 llamadas simultáneas async def call(self, agent_key: str, messages: list, temperature: float = 0.7): agent = AGENT_REGISTRY.get(agent_key) if not agent: raise ValueError(f"Agente {agent_key} no encontrado") provider = PROVIDERS[agent["provider"]] system_prompt = agent["role"] # Mensajes completos (system + history) full_messages = [{"role": "system", "content": system_prompt}] + messages if provider["type"] == "gemini": # ── Soporte para Google Gemini ─────────────────────────────────────── try: import google.generativeai as genai genai.configure(api_key=provider["key"]) model_name = agent["models"][0] try: model = genai.GenerativeModel(model_name) except Exception as e: print(f"Modelo {model_name} falló: {e}") model_name = "gemini-2.5-flash" # fallback duro model = genai.GenerativeModel(model_name) # Convertir formato OpenAI → Gemini gemini_messages = [] for msg in full_messages: role = "user" if msg["role"] in ("system", "user") else "model" gemini_messages.append({ "role": role, "parts": [{"text": msg["content"]}] }) response = await asyncio.to_thread( model.generate_content, gemini_messages, generation_config={"temperature": temperature, "max_output_tokens": 4096} ) return response.text except ImportError: raise ImportError("Instala google-generativeai: pip install google-generativeai") except Exception as e: print(f"Error en Gemini ({model_name}): {str(e)}") raise elif provider["type"] == "openai_compat": # ── Soporte para OpenRouter, Groq, etc. ─────────────────────────────── @retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=2, max=30), retry=retry_if_exception_type((httpx.HTTPStatusError, httpx.TimeoutException)) ) async def try_provider(base_url, model, api_key, headers): async with self.semaphore: payload = { "model": model, "messages": full_messages, "temperature": temperature, "max_tokens": 4096, "top_p": 0.92 } async with httpx.AsyncClient(timeout=90) as client: resp = await client.post( f"{base_url}/chat/completions", headers={"Authorization": f"Bearer {api_key}", **headers}, json=payload ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] # Intentar modelos del proveedor principal last_err = None for model in agent.get("models", []): try: return await try_provider( provider["base_url"], model, provider["key"], provider.get("headers", {}) ) except Exception as e: last_err = e print(f"Error con {model}: {str(e)}") # Fallback a OpenRouter si no es el principal if agent["provider"] != "openrouter" and PROVIDERS["openrouter"]["key"]: or_p = PROVIDERS["openrouter"] for model in or_p["models"]: try: return await try_provider( or_p["base_url"], model, or_p["key"], or_p.get("headers", {}) ) except: continue raise Exception(f"Todos los proveedores fallaron. Último error: {last_err}") else: raise ValueError(f"Tipo de proveedor desconocido: {provider['type']}") # Instancia global llm = LLMClient()