import requests import logging import re import os from datetime import datetime # Configuración de logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("AI-Plugin") # Variables de entorno necesarias HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY") BRAVE_API_KEY = os.getenv("BRAVE_API_KEY") MODEL_ID = os.getenv("HUGGINGFACE_MODEL", "deepseek/deepseek-v3-0324") # Endpoints HF_API_URL = "https://router.huggingface.co/novita/v3/openai/chat/completions" BRAVE_URL = "https://api.search.brave.com/res/v1/web/search" # Headers HEADERS_HF = { "Authorization": f"Bearer {HUGGINGFACE_API_KEY}", "Content-Type": "application/json" } HEADERS_BRAVE = { "Accept": "application/json", "X-Subscription-Token": BRAVE_API_KEY } def respuesta_desactualizada(texto): """Detecta si la respuesta contiene fechas antiguas en relación al mes/año actual.""" texto = texto.lower() ahora = datetime.now() # Lista de años pasados (2020 hasta el año anterior) anyos_pasados = [str(a) for a in range(2020, ahora.year)] if any(a in texto for a in anyos_pasados): return True # Lista de meses pasados del mismo año (enero - mes anterior) meses_ordenados = [ "enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre" ] mes_actual_idx = ahora.month - 1 meses_pasados = meses_ordenados[:mes_actual_idx] for mes in meses_pasados: if f"{mes} de {ahora.year}" in texto: return True return False def sanitize_response(text, remove_first_line=True): """Limpia etiquetas HTML y fragmentos redundantes.""" clean_text = re.sub(r"<.*?>", "", text) clean_text = re.sub(r""", '"', clean_text) clean_text = re.sub(r"&", "&", clean_text) clean_text = clean_text.strip() instrucciones_a_eliminar = [ "Resume y humaniza esta información en lenguaje natural:", "Responde en español:", "A continuación tienes un conjunto de textos o fragmentos sacados de sitios web. Tu tarea es analizarlos, eliminar información redundante o poco útil, y resumir todo en un único párrafo claro y natural para un humano. No repitas los títulos ni hagas listas. Explica como si le contaras a un amigo:" ] for instruccion in instrucciones_a_eliminar: if clean_text.lower().startswith(instruccion.lower()): clean_text = clean_text[len(instruccion):].strip() return clean_text def query_huggingface(prompt, is_humanization=False): """Consulta el modelo DeepSeek en Hugging Face Router.""" if not HUGGINGFACE_API_KEY: logger.error("No se ha definido la variable HUGGINGFACE_API_KEY.") return None # Incluir fecha real en el prompt del usuario fecha_actual = datetime.now().strftime("%d de %B de %Y") # ej: "11 de abril de 2025" prompt_con_fecha = f"Hoy es {fecha_actual}. {prompt.strip()}" # Prompt de sistema personalizado if is_humanization: system_prompt = ( "Tu tarea es analizar fragmentos de sitios web y resumir la información relevante " "en un único párrafo claro, sin repetir títulos ni hacer listas. Sé directo, preciso y " "habla como si lo explicaras a un amigo en español." ) else: system_prompt = ( "Eres un asistente útil que siempre responde en español. " "Ignora nombres de usuario como 'teraflops'. " "Si no conoces información actualizada a la fecha de hoy, indica que no estás seguro. " "No inventes datos si no los sabes." ) payload = { "model": MODEL_ID, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt_con_fecha} ], "temperature": 0.7, "max_tokens": 300 } try: response = requests.post(HF_API_URL, headers=HEADERS_HF, json=payload, timeout=120) response.raise_for_status() content = response.json()["choices"][0]["message"]["content"] logger.info(f"[DEBUG] Respuesta cruda de Hugging Face:\n{content}") return sanitize_response(content) except requests.exceptions.RequestException as e: logger.error(f"[ERROR] Fallo al conectar con Hugging Face: {e}") return None def buscar_en_brave(consulta): """Consulta Brave Search y decide si humanizar o mostrar mensaje de fallback.""" if not BRAVE_API_KEY: logger.error("No se ha definido la variable BRAVE_API_KEY.") return None params = {"q": consulta, "count": 3, "safesearch": "moderate"} try: logger.info(f"[DEBUG] Consultando Brave Search con: {consulta}") response = requests.get(BRAVE_URL, headers=HEADERS_BRAVE, params=params, timeout=30) response.raise_for_status() data = response.json() resultados = data.get("web", {}).get("results", []) if not resultados: logger.warning("Brave no devolvió resultados.") return "No encontré información relevante en Brave Search." informacion_bruta = "\n".join( f"{res.get('title', 'Sin título')}: {res.get('description', 'Sin descripción')}" for res in resultados ) logger.info(f"[DEBUG] Resultados de Brave crudos:\n{informacion_bruta}") # Palabras clave genéricas que suelen indicar contenido útil palabras_utiles = [ "2025", "abril", "marzo", "versión", "version", "released", "linux", "resultado", "marcador", "gol", "victoria", "derrota", "ganó", "1-0", "2-1", "empate", "directo", "esta noche", "hoy", "final", "resumen", "publicado", "disponible", "estreno", "actualización", "presentación", "valencia", "sevilla" ] texto_brave = informacion_bruta.lower() pistas_utiles = any(p in texto_brave for p in palabras_utiles) if not pistas_utiles: logger.info("[DEBUG] Brave devolvió resultados ambiguos.") return ( "📡 Brave Search encontró resultados poco claros. " "No se halló información confirmada. Puedes revisar sitios oficiales para más detalles." ) # Si hay contenido con pistas útiles, lo pasamos a Hugging Face para humanizar resumen = query_huggingface(sanitize_response(informacion_bruta), is_humanization=True) return resumen except requests.exceptions.RequestException as e: logger.error(f"[ERROR] Fallo en Brave Search: {e}") return None def procesar_consulta(prompt): """Intenta responder usando HF, y si falla, pregunta a Brave.""" logger.info(f"[DEBUG] ⏳ Consultando Hugging Face con: {prompt}") respuesta_hf = query_huggingface(prompt) if not respuesta_hf: return "No se obtuvo respuesta de Hugging Face." patron_generica = re.compile( r"(no hay información.*?|" r"no tengo.*?información|" r"consulta.*?sitios? web oficiales|" r"aún no se ha anunciado|" r"lo siento, pero no puedo responder a eso|" r"no tengo suficiente información|" r"no puedo proporcionar esa información|" r"te recomiendo verificar fuentes oficiales)", re.IGNORECASE ) # 🔧 Este bloque estaba fuera de la función — lo metemos correctamente dentro if patron_generica.search(respuesta_hf) or respuesta_desactualizada(respuesta_hf): logger.info("[DEBUG] Hugging Face dio respuesta genérica o desactualizada, consultando Brave...") respuesta_brave = buscar_en_brave(prompt) return f"📡 Fuente: Brave Search\n{respuesta_brave or 'No se encontró información útil.'}" return f"🧠 Fuente: Hugging Face\n{respuesta_hf}" def run(sender, *args): """Manejador del comando .ai""" if not args: return "Por favor, proporciona una consulta." consulta = " ".join(args).strip() logger.info(f"[.ai] Procesando consulta: {consulta}") respuesta = procesar_consulta(consulta) return respuesta or "No se encontró información disponible." def help(): return "Uso: .ai - Responde con IA usando Hugging Face y Brave Search si es necesario."