222 lines
8.2 KiB
Python
222 lines
8.2 KiB
Python
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 <pregunta> - Responde con IA usando Hugging Face y Brave Search si es necesario."
|
|
|