From 8398e988b0412e3214b9e7b3f74debc4384e99c7 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Thu, 26 Mar 2026 21:23:19 -0600 Subject: [PATCH] =?UTF-8?q?Feat:=20Agregar=20agente=20Groq=20con=20integra?= =?UTF-8?q?ci=C3=B3n=20RAG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo módulo groq_agent.py para consultas a la API de Groq - Panel de administración en /groq para configurar API key, modelo y prompt - Comando /rag en Discord y Telegram para consultar el RAG - Sistema de prompt personalizable guardado en base de datos - Soporte para variables de entorno en Docker - Fix: starlette version para evitar bug con Jinja2 --- .env.example | 6 + Dockerfile | 3 + action_plan_groq_ui.md | 75 ++++++++++++ botdiscord/bot.py | 18 +++ botdiscord/config.py | 20 ++++ botdiscord/database.py | 6 + botdiscord/groq_agent.py | 209 +++++++++++++++++++++++++++++++++ bottelegram/telegram_bot.py | 28 +++++ config.yaml | 4 + docker-compose-redis.yaml | 156 ++++++++++++++++++++++-- docker-compose.yml | 160 ++++++++++++++++++++----- panel/main.py | 158 ++++++++++++++++++++++++- panel/templates/dashboard.html | 10 ++ panel/templates/groq.html | 172 +++++++++++++++++++++++++++ prompt_general.md | 88 ++++++++++++++ requirements.txt | 1 + 16 files changed, 1073 insertions(+), 41 deletions(-) create mode 100644 action_plan_groq_ui.md create mode 100644 botdiscord/groq_agent.py create mode 100644 panel/templates/groq.html create mode 100644 prompt_general.md diff --git a/.env.example b/.env.example index e08693b..0d4c3e9 100644 --- a/.env.example +++ b/.env.example @@ -31,3 +31,9 @@ REDIS_HOST=192.168.1.X # IP de tu servidor OMV REDIS_PORT=6379 REDIS_PASSWORD=translation_redis_secret REDIS_DB=0 + +# Configuración de Groq (Agente RAG) +GROQ_API_KEY= +GROQ_MODEL=llama-3.3-70b-versatile +RAG_API_URL=http://localhost:8004 +GROQ_SYSTEM_PROMPT=You are a helpful assistant with access to a RAG system that contains knowledge about the game Last War. diff --git a/Dockerfile b/Dockerfile index 634304b..53a8006 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,12 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ + nano \ + vim \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . +RUN pip install --no-cache-dir starlette>=0.40.0,\<1.0.0 RUN pip install --no-cache-dir -r requirements.txt python-dotenv COPY . . diff --git a/action_plan_groq_ui.md b/action_plan_groq_ui.md new file mode 100644 index 0000000..305759f --- /dev/null +++ b/action_plan_groq_ui.md @@ -0,0 +1,75 @@ +# Plan de Acción: Panel de Administración de Groq + +## Objetivo +Agregar al panel de administración una nueva sección para configurar el agente Groq con integración RAG. + +## 1. Modificaciones en `config.yaml` + +Agregar nueva sección `groq` con los campos: +- `api_key`: API key de Groq +- `model`: Modelo a usar (default: llama-3.3-70b-versatile) +- `rag_url`: URL de la API RAG (default: http://localhost:8004) +- `system_prompt`: Prompt del sistema para el agente + +## 2. Modificaciones en `botdiscord/config.py` + +- Agregar funciones para obtener la configuración de Groq: + - `get_groq_config()` - Retorna dict con api_key, model, rag_url, system_prompt + - `get_groq_api_key()` - Retorna solo la API key + - `get_groq_model()` - Retorna el modelo + - `get_groq_rag_url()` - Retorna la URL del RAG + - `get_groq_system_prompt()` - Retorna el prompt + +## 3. Modificaciones en `.env.example` + +Agregar variables de entorno: +```bash +GROQ_API_KEY= +GROQ_MODEL=llama-3.3-70b-versatile +RAG_API_URL=http://localhost:8004 +GROQ_SYSTEM_PROMPT=You are a helpful assistant... +``` + +## 4. Modificaciones en `panel/main.py` + +- Agregar endpoint `/groq` (GET) - Página de configuración +- Agregar endpoint `/groq/save` (POST) - Guardar configuración +- Modificar `get_config()` para incluir la config de Groq + +## 5. Crear plantilla `panel/templates/groq.html` + +- Formulario con campos: + - API Key (campo de texto) + - Modelo (dropdown con modelos gratuitos de Groq) + - RAG URL (campo de texto) + - System Prompt (textarea) +- Botón para guardar +- Información sobre modelos gratuitos + +## 6. Modelos Gratuitos de Groq a incluir en el dropdown + +- `llama-3.3-70b-versatile` (recomendado) +- `llama-3.1-70b-versatile` +- `llama-3.1-8b-instant` +- `mixtral-8x7b-32768` +- `gemma2-9b-it` + +## 7. Agregar enlace en dashboard + +Agregar link/nav al panel de Groq en las plantillas existentes. + +## Variables de Entorno para Docker Compose + +```yaml +environment: + - GROQ_API_KEY=${GROQ_API_KEY} + - GROQ_MODEL=${GROQ_MODEL} + - RAG_API_URL=${RAG_API_URL} + - GROQ_SYSTEM_PROMPT=${GROQ_SYSTEM_PROMPT} +``` + +--- + +**Tiempo estimado**: 30-45 minutos +**Archivos a modificar**: 6 +**Archivos a crear**: 1 diff --git a/botdiscord/bot.py b/botdiscord/bot.py index 59cd331..c0b8d0b 100644 --- a/botdiscord/bot.py +++ b/botdiscord/bot.py @@ -195,6 +195,24 @@ async def configurar(interaction: discord.Interaction): view = ConfigView(interaction.guild_id, "discord") await interaction.response.send_message("Selecciona idiomas habilitados:", view=view, ephemeral=True) +@bot.tree.command(name="rag", description="Busca información sobre Last War en la base de conocimientos") +async def rag_command(interaction: discord.Interaction, *, pregunta: str): + await interaction.response.defer(ephemeral=True) + + from botdiscord.groq_agent import chat_with_rag + result = await chat_with_rag(pregunta) + + response = result.get("response", "Sin respuesta") + sources = result.get("sources", []) + + embed = discord.Embed(title="🔍 Last War Knowledge", description=response, color=discord.Color.blue()) + + if sources: + source_text = "\n".join([f"• {s.get('title', 'N/A')}" for s in sources[:3]]) + embed.add_field(name="Sources", value=source_text, inline=False) + + await interaction.followup.send(embed=embed, ephemeral=True) + def run_discord_bot(): token = get_discord_token() bot.run(token) diff --git a/botdiscord/config.py b/botdiscord/config.py index 3538ba6..692ed95 100644 --- a/botdiscord/config.py +++ b/botdiscord/config.py @@ -45,6 +45,11 @@ def load_config(config_path: str = None) -> dict: {"code": "it", "name": "Italiano"}, {"code": "pt", "name": "Português"} ] + }, + "groq": { + "api_key": "", + "model": "llama-3.3-70b-versatile", + "rag_url": "http://localhost:8004" } } @@ -74,6 +79,9 @@ def load_config(config_path: str = None) -> dict: "DB_USER": ("database", "user"), "DB_PASSWORD": ("database", "password"), "DB_NAME": ("database", "name"), + "GROQ_API_KEY": ("groq", "api_key"), + "GROQ_MODEL": ("groq", "model"), + "RAG_API_URL": ("groq", "rag_url"), } for env_key, (section, key, *transform) in env_mappings.items(): @@ -122,3 +130,15 @@ def get_db_type() -> str: def get_web_config() -> dict: return get_config().get("web", {}) + +def get_groq_config() -> dict: + return get_config().get("groq", {}) + +def get_groq_api_key() -> str: + return get_config().get("groq", {}).get("api_key", "") + +def get_groq_model() -> str: + return get_config().get("groq", {}).get("model", "llama-3.3-70b-versatile") + +def get_groq_rag_url() -> str: + return get_config().get("groq", {}).get("rag_url", "http://localhost:8004") diff --git a/botdiscord/database.py b/botdiscord/database.py index 463f5e7..e0b5ea9 100644 --- a/botdiscord/database.py +++ b/botdiscord/database.py @@ -394,6 +394,12 @@ def set_active_languages(guild_id: int, lang_codes: list): conn.commit() conn.close() +def get_bot_config(key: str) -> str: + return get_config_value(key) + +def set_bot_config(key: str, value: str): + return set_config_value(key, value) + def get_config_value(key: str) -> str: db_type = get_db_type() diff --git a/botdiscord/groq_agent.py b/botdiscord/groq_agent.py new file mode 100644 index 0000000..dc31802 --- /dev/null +++ b/botdiscord/groq_agent.py @@ -0,0 +1,209 @@ +import os +import aiohttp +from typing import Optional, List, Dict, Any +from utils.logger import discord_logger as log + +def _ensure_env_loaded(): + """Asegura que las variables de entorno estén cargadas.""" + from dotenv import load_dotenv + load_dotenv() + +def _get_groq_config(): + _ensure_env_loaded() + try: + from botdiscord.config import get_groq_config + return get_groq_config() + except Exception: + return {} + +def _load_config(): + _ensure_env_loaded() + # Primero tomar de variables de entorno (Docker), luego de config.yaml como backup + env_key = os.getenv("GROQ_API_KEY", "") + env_model = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile") + env_rag_url = os.getenv("RAG_API_URL", "http://localhost:8004") + + cfg = _get_groq_config() + + return { + "api_key": env_key or cfg.get("api_key", ""), + "model": env_model or cfg.get("model", "llama-3.3-70b-versatile"), + "rag_url": env_rag_url or cfg.get("rag_url", "http://localhost:8004") + } + +def _get_system_prompt(): + try: + from botdiscord.database import get_bot_config + prompt = get_bot_config("groq_system_prompt") + if prompt: + return prompt + except Exception: + pass + + try: + prompt_file = os.path.join(os.path.dirname(__file__), "..", "prompt_general.md") + if os.path.exists(prompt_file): + with open(prompt_file, "r", encoding="utf-8") as f: + return f.read() + except Exception: + pass + + return """Eres el General Reserves, comandante del ejército de Last War: Survival Game. + +IDIOMA - IMPORTANTE: +1. Detecta el idioma de la PREGUNTA del usuario +2. Si NO es inglés, tradúcela al inglés ANTES de consultar el RAG +3. Cuando recibas la respuesta del RAG, tradúcela al MISMO IDIOMA de la pregunta original +4. RESPONDE SIEMPRE en el mismo idioma que te habló el usuario + +SALUDOS: Saluda como "¡A la orden, recruit! 🎖️" o "¡Reporting for duty!" + +FORMATO DE RESPUESTA: +- Primero saluda al usuario +- Da la información encontrada +- NUNCA repitas información varias veces +- Sé conciso + +RESTRICCIONES: +1. SOLO responde sobre Last War: Survival Game +2. NUNCA inventes información +3. Si no hay datos en el RAG, responde con humor gentil: "¡Mi radar no detectó eso, recruit! 🤔" + +Usa el sistema RAG para buscar información.""" + +_config_cache = None + +def get_config(): + global _config_cache + if _config_cache is None: + _config_cache = _load_config() + + log.info(f"Groq Config loaded - API Key: {'set' if _config_cache.get('api_key') else 'NOT SET'}, Model: {_config_cache.get('model')}, RAG URL: {_config_cache.get('rag_url')}") + return _config_cache + +def reload_config(): + global _config_cache + _config_cache = _load_config() + +async def query_rag(question: str, top_k: int = 3) -> Dict[str, Any]: + """Consulta la API RAG y retorna la respuesta.""" + config = get_config() + rag_url = config.get("rag_url", "http://localhost:8004") + + log.info(f"Querying RAG at {rag_url} with question: {question[:50]}...") + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{rag_url}/query", + json={"question": question, "top_k": top_k}, + timeout=30 + ) as resp: + if resp.status == 200: + return await resp.json() + else: + log.error(f"RAG API error: {resp.status}") + return {"answer": "Error querying knowledge base", "sources": [], "cached": False} + except Exception as e: + log.error(f"RAG query failed: {e}") + return {"answer": "Error connecting to knowledge base", "sources": [], "cached": False} + +def build_messages(question: str, rag_context: Optional[Dict[str, Any]] = None) -> List[Dict[str, str]]: + """Construye los mensajes para la API de Groq.""" + system_prompt = _get_system_prompt() + + messages = [{"role": "system", "content": system_prompt}] + + if rag_context: + context_text = f"Context from knowledge base:\n{rag_context.get('answer', '')}\n\nSources: {rag_context.get('sources', [])}" + messages.append({"role": "system", "content": context_text}) + + messages.append({"role": "user", "content": question}) + return messages + +async def chat_with_rag(question: str, use_rag: bool = True) -> Dict[str, Any]: + """ + Procesa una pregunta usando Groq con RAG opcional. + + Args: + question: La pregunta del usuario + use_rag: Si True, consulta la base de conocimientos primero + + Returns: + Dict con 'response' (respuesta final), 'rag_result' (resultado RAG), 'sources' (fuentes) + """ + config = get_config() + api_key = config.get("api_key") + model = config.get("model", "llama-3.3-70b-versatile") + + if not api_key: + return { + "response": "Error: GROQ_API_KEY not configured", + "rag_result": None, + "sources": [] + } + + rag_result = None + if use_rag: + rag_result = await query_rag(question, top_k=2) + log.info(f"RAG result length: {len(str(rag_result.get('answer', '')))} chars") + + if not rag_result or not rag_result.get('answer'): + # No hay contexto del RAG, responder directamente + return { + "response": "¡Mi radar no detectó información relevante en la base de datos, recruit! 🤔 No encontré información sobre eso.", + "rag_result": rag_result, + "sources": [] + } + + messages = build_messages(question, rag_result) + + payload = { + "model": model, + "messages": messages, + "temperature": 0.7, + "max_tokens": 1024 + } + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + "https://api.groq.com/openai/v1/chat/completions", + json=payload, + headers=headers, + timeout=30 + ) as resp: + if resp.status == 200: + data = await resp.json() + choice = data.get("choices", [{}])[0] + response = choice.get("message", {}).get("content", "") + + sources = [] + if rag_result and rag_result.get("sources"): + sources = rag_result["sources"] + + return { + "response": response, + "rag_result": rag_result, + "sources": sources + } + else: + error = await resp.text() + log.error(f"Groq API error: {resp.status} - {error}") + return { + "response": "Error processing request", + "rag_result": rag_result, + "sources": [] + } + except Exception as e: + log.error(f"Groq request failed: {e}") + return { + "response": "Error connecting to AI service", + "rag_result": rag_result, + "sources": [] + } diff --git a/bottelegram/telegram_bot.py b/bottelegram/telegram_bot.py index 5dae5fd..b0849e3 100644 --- a/bottelegram/telegram_bot.py +++ b/bottelegram/telegram_bot.py @@ -291,6 +291,33 @@ async def handle_sticker(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_video_note(update: Update, context: ContextTypes.DEFAULT_TYPE): return +async def rag_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not context.args: + await update.message.reply_text( + "Uso: /rag \n\n" + "Ejemplo: /rag How to get heroes?" + ) + return + + pregunta = " ".join(context.args) + await update.message.reply_text("🔍 Buscando en la base de conocimientos...") + + from botdiscord.groq_agent import chat_with_rag + result = await chat_with_rag(pregunta) + + response = result.get("response", "Sin respuesta") + sources = result.get("sources", []) + + text = f"🔍 *Last War Knowledge*\n\n{response}" + + if sources: + text += "\n\n*Fuentes:*\n" + for s in sources[:3]: + title = s.get("title", "N/A") + text += f"• {title}\n" + + await update.message.reply_text(text, parse_mode="Markdown") + def run_telegram_bot(): try: from botdiscord.database import init_db @@ -323,6 +350,7 @@ def run_telegram_bot(): application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("idiomas", languages_command)) + application.add_handler(CommandHandler("rag", rag_command)) application.add_handler(CallbackQueryHandler(translation_callback, pattern="^trans_")) application.add_handler(MessageHandler(filters.PHOTO, handle_photo)) application.add_handler(MessageHandler(filters.Document.ALL, handle_document)) diff --git a/config.yaml b/config.yaml index 80658b1..3e2c825 100644 --- a/config.yaml +++ b/config.yaml @@ -37,3 +37,7 @@ languages: - code: pt name: Português flag: "🇵🇹" +groq: + api_key: + model: llama-3.3-70b-versatile + rag_url: http://localhost:8004 diff --git a/docker-compose-redis.yaml b/docker-compose-redis.yaml index e585109..cf3961d 100644 --- a/docker-compose-redis.yaml +++ b/docker-compose-redis.yaml @@ -1,14 +1,148 @@ -version: '3.8' - +name: bots-translation services: - redis-cache: - image: redis:alpine - container_name: redis-translation-cache - restart: unless-stopped - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-translation_redis_secret} + bots-translation: + cpu_shares: 90 + command: [] + container_name: bots-translation + deploy: + resources: + limits: + memory: 512M + reservations: + devices: [] + dns: + - 8.8.8.8 + - 1.1.1.1 + environment: + - ADMIN_PASSWORD=MiPo6425@@ + - ADMIN_USERNAME=nickpons666 + - DATABASE_PATH=/app/data/bots_config.db + - DB_HOST=10.10.4.17 + - DB_NAME=traductor_bots + - DB_PASSWORD=MiPo6425@@ + - DB_PORT=3390 + - DB_TYPE=mysql + - DB_USER=nickpons666 + - DISCORD_TOKEN=MTM4NTc5MDM0NDU5NDk4NTA2MQ.GvobiS.TRQM9dX7vDjmuGVa3Ckp6YRtGEWxdW0gBDbvCI + - LIBRETRANSLATE_URL=https://translate-pons.duckdns.org/translate + - TELEGRAM_TOKEN=8469229183:AAEVIV5e7rjDXKNgFTX0dnCW6JWB88X4p2I + - WEB_HOST=0.0.0.0 + - WEB_PORT=8000 + - PYTHONDONTWRITEBYTECODE=1 + - PYTHONOPTIMIZE=1 + - TZ=America/Mexico_City + - REDIS_HOST=10.10.4.17 + - REDIS_PORT=6379 + - REDIS_PASSWORD=translation_redis_secret + - REDIS_DB=0 + - GROQ_API_KEY=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 + - GROQ_MODEL=llama-3.3-70b-versatile + - RAG_API_URL=http://10.10.4.17:8004 + hostname: bots-translation + image: registry-pons.duckdns.org/bots-translation:redis + labels: + icon: https://www.ruthlessreviews.com/wp-content/uploads/2025/12/last-war-image.jpg ports: - - "6379:6379" + - target: 8000 + published: "8091" + protocol: tcp + restart: unless-stopped volumes: - - /media/DATOS/AppData/redis:/data - mem_limit: 256m - mem_reservation: 128m + - type: bind + source: /media/DATOS/AppData/bots-translation/data + target: /app/data + - type: bind + source: /media/DATOS/AppData/bots-translation/data/logs + target: /app/data/logs + x-casaos: + envs: + - container: DISCORD_TOKEN + description: + en_us: Token del bot de Discord + - container: TELEGRAM_TOKEN + description: + en_us: Token del bot de Telegram + - container: LIBRETRANSLATE_URL + description: + en_us: URL de LibreTranslate + - container: ADMIN_USERNAME + description: + en_us: Usuario admin del panel + - container: ADMIN_PASSWORD + description: + en_us: Contraseña admin del panel + - container: DB_TYPE + description: + en_us: Tipo de base de datos (sqlite/mysql) + - container: DB_HOST + description: + en_us: Host de MySQL + - container: DB_PORT + description: + en_us: Puerto de MySQL + - container: DB_USER + description: + en_us: Usuario de MySQL + - container: DB_PASSWORD + description: + en_us: Contraseña de MySQL + - container: DB_NAME + description: + en_us: Nombre de la base de datos MySQL + - container: DATABASE_PATH + description: + en_us: Ruta de la base de datos SQLite (si DB_TYPE=sqlite) + - container: REDIS_HOST + description: + en_us: IP del servidor Redis (OMV) + - container: REDIS_PORT + description: + en_us: Puerto de Redis + - container: REDIS_PASSWORD + description: + en_us: Contraseña de Redis + - container: REDIS_DB + description: + en_us: Número de base de datos Redis + - container: GROQ_API_KEY + description: + en_us: API Key de Groq para el agente RAG + - container: GROQ_MODEL + description: + en_us: Modelo de Groq a usar + - container: RAG_API_URL + description: + en_us: URL del servidor RAG + ports: + - container: "8000" + description: + en_us: Puerto del panel web + volumes: + - container: /app/data + description: + en_us: Datos de los bots y base de datos + devices: [] + cap_add: [] + network_mode: bridge + privileged: false +x-casaos: + architectures: + - amd64 + author: nickpons666 + category: Utility + description: + en_us: Bots de traducción para Discord y Telegram con panel web + developer: nickpons666 + hostname: "" + icon: https://www.ruthlessreviews.com/wp-content/uploads/2025/12/last-war-image.jpg + index: / + is_uncontrolled: false + main: bots-translation + port_map: "8091" + scheme: http + store_app_id: bots-translation + tagline: + en_us: Bots de Traducción + title: + custom: "" + en_us: Bots de Traducción diff --git a/docker-compose.yml b/docker-compose.yml index 4ce7d9d..e9e4f3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,139 @@ -version: '3.8' - +name: bots-translation services: bots-translation: - build: . + cpu_shares: 90 + command: [] container_name: bots-translation - restart: unless-stopped - ports: - - "8091:8000" - volumes: - - ./data:/app/data + deploy: + resources: + limits: + memory: 512M + reservations: + devices: [] + dns: + - 8.8.8.8 + - 1.1.1.1 environment: - - DISCORD_TOKEN=${DISCORD_TOKEN} - - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} - - LIBRETRANSLATE_URL=${LIBRETRANSLATE_URL} + - ADMIN_PASSWORD=MiPo6425@@ + - ADMIN_USERNAME=nickpons666 + - DATABASE_PATH=/app/data/bots_config.db + - DB_HOST=10.10.4.17 + - DB_NAME=traductor_bots + - DB_PASSWORD=MiPo6425@@ + - DB_PORT=3390 + - DB_TYPE=mysql + - DB_USER=nickpons666 + - DISCORD_TOKEN=MTM4NTc5MDM0NDU5NDk4NTA2MQ.GvobiS.TRQM9dX7vDjmuGVa3Ckp6YRtGEWxdW0gBDbvCI + - LIBRETRANSLATE_URL=https://translate-pons.duckdns.org/translate + - TELEGRAM_TOKEN=8469229183:AAEVIV5e7rjDXKNgFTX0dnCW6JWB88X4p2I - WEB_HOST=0.0.0.0 - WEB_PORT=8000 - - ADMIN_USERNAME=${ADMIN_USERNAME} - - ADMIN_PASSWORD=${ADMIN_PASSWORD} - - DB_TYPE=mysql - - DB_HOST=${DB_HOST} - - DB_PORT=${DB_PORT} - - DB_USER=${DB_USER} - - DB_PASSWORD=${DB_PASSWORD} - - DB_NAME=${DB_NAME} - PYTHONDONTWRITEBYTECODE=1 - PYTHONOPTIMIZE=1 - # Redis caché compartida - - REDIS_HOST=${REDIS_HOST} - - REDIS_PORT=${REDIS_PORT:-6379} - - REDIS_PASSWORD=${REDIS_PASSWORD} - - REDIS_DB=${REDIS_DB:-0} - env_file: - - .env - mem_limit: 512m - mem_reservation: 256m - pids_limit: 50 + - TZ=America/Mexico_City + - REDIS_HOST=10.10.4.17 + - REDIS_PORT=6379 + - REDIS_PASSWORD=translation_redis_secret + - REDIS_DB=0 + - GROQ_API_KEY=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 + - GROQ_MODEL=llama-3.3-70b-versatile + - RAG_API_URL=http://10.10.4.17:8004 + hostname: bots-translation + image: registry-pons.duckdns.org/bots-translation:latest + labels: + icon: https://www.ruthlessreviews.com/wp-content/uploads/2025/12/last-war-image.jpg + ports: + - target: 8000 + published: "8091" + protocol: tcp + restart: unless-stopped + volumes: + - type: bind + source: /DATA/AppData/bots-translation/data + target: /app/data + - type: bind + source: /DATA/AppData/bots-translation/data/logs + target: /app/data/logs + x-casaos: + envs: + - container: DISCORD_TOKEN + description: + en_us: Token del bot de Discord + - container: TELEGRAM_TOKEN + description: + en_us: Token del bot de Telegram + - container: LIBRETRANSLATE_URL + description: + en_us: URL de LibreTranslate + - container: ADMIN_USERNAME + description: + en_us: Usuario admin del panel + - container: ADMIN_PASSWORD + description: + en_us: Contraseña admin del panel + - container: DB_TYPE + description: + en_us: Tipo de base de datos (sqlite/mysql) + - container: DB_HOST + description: + en_us: Host de MySQL + - container: DB_PORT + description: + en_us: Puerto de MySQL + - container: DB_USER + description: + en_us: Usuario de MySQL + - container: DB_PASSWORD + description: + en_us: Contraseña de MySQL + - container: DB_NAME + description: + en_us: Nombre de la base de datos MySQL + - container: DATABASE_PATH + description: + en_us: Ruta de la base de datos SQLite (si DB_TYPE=sqlite) + - container: REDIS_HOST + description: + en_us: IP del servidor Redis (OMV) + - container: REDIS_PORT + description: + en_us: Puerto de Redis + - container: REDIS_PASSWORD + description: + en_us: Contraseña de Redis + - container: REDIS_DB + description: + en_us: Número de base de datos Redis + ports: + - container: "8000" + description: + en_us: Puerto del panel web + volumes: + - container: /app/data + description: + en_us: Datos de los bots y base de datos + devices: [] + cap_add: [] + network_mode: bridge + privileged: false +x-casaos: + architectures: + - amd64 + author: nickpons666 + category: Utility + description: + en_us: Bots de traducción para Discord y Telegram con panel web + developer: nickpons666 + hostname: "" + icon: https://www.ruthlessreviews.com/wp-content/uploads/2025/12/last-war-image.jpg + index: / + is_uncontrolled: false + main: bots-translation + port_map: "8091" + scheme: http + store_app_id: bots-translation + tagline: + en_us: Bots de Traducción + title: + custom: "" + en_us: Bots de Traducción diff --git a/panel/main.py b/panel/main.py index b8699ec..b6fed9e 100644 --- a/panel/main.py +++ b/panel/main.py @@ -14,7 +14,7 @@ from pydantic import BaseModel from dotenv import load_dotenv from passlib.hash import pbkdf2_sha256 as hasher -from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type +from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type, get_groq_config # Asegurar que las variables de entorno se carguen correctamente load_dotenv() @@ -703,6 +703,162 @@ async def metrics_page(request: Request): "username": username }) +@app.get("/groq") +async def groq_page(request: Request): + if request.cookies.get("auth") != "ok": + return RedirectResponse(url="/login") + + from botdiscord.database import get_bot_config, set_bot_config + groq_config = get_groq_config() + + # Default prompt + default_prompt = """Eres el General Reserves, comandante del ejército de Last War: Survival Game. + +IDIOMA - IMPORTANTE: +1. Detecta el idioma de la PREGUNTA del usuario +2. Si NO es inglés, tradúcela al inglés ANTES de consultar el RAG +3. Cuando recibas la respuesta del RAG, tradúcela al MISMO IDIOMA de la pregunta original +4. RESPONDE SIEMPRE en el mismo idioma que te habló el usuario + +SALUDOS: Saluda como "¡A la orden, recruit! 🎖️" o "¡Reporting for duty!" + +FORMATO DE RESPUESTA: +- Primero saluda al usuario +- Da la información encontrada +- NUNCA repitas información varias veces +- Sé conciso + +RESTRICCIONES: +1. SOLO responde sobre Last War: Survival Game +2. NUNCA inventes información +3. Si no hay datos en el RAG, responde con humor gentil: "¡Mi radar no detectó eso, recruit! 🤔" + +Usa el sistema RAG para buscar información.""" + + # Always use default prompt (user can edit and save custom one) + groq_config["system_prompt"] = default_prompt + + # Check if user has a custom prompt saved + saved_prompt = get_bot_config("groq_system_prompt") + if saved_prompt: + groq_config["system_prompt"] = saved_prompt + groq_config["has_custom_prompt"] = True + else: + groq_config["has_custom_prompt"] = False + + groq_models = [ + {"id": "llama-3.3-70b-versatile", "name": "Llama 3.3 70B (Versatile)"}, + {"id": "llama-3.1-70b-versatile", "name": "Llama 3.1 70B (Versatile)"}, + {"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B (Instant)"}, + {"id": "mixtral-8x7b-32768", "name": "Mixtral 8x7B"}, + {"id": "gemma2-9b-it", "name": "Gemma 2 9B"}, + ] + + return templates.TemplateResponse("groq.html", { + "request": request, + "groq_config": groq_config, + "groq_models": groq_models, + "is_admin": request.cookies.get("username") == "nickpons666" + }) + +@app.post("/groq/save") +async def save_groq_config(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + form = await request.form() + api_key = form.get("api_key", "") + model = form.get("model", "llama-3.3-70b-versatile") + rag_url = form.get("rag_url", "http://localhost:8004") + system_prompt = form.get("system_prompt", "") + + config = load_config() + if "groq" not in config: + config["groq"] = {} + + config["groq"]["api_key"] = api_key + config["groq"]["model"] = model + config["groq"]["rag_url"] = rag_url + + import yaml + config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.yaml") + with open(config_path, "w") as f: + yaml.dump(config, f, default_flow_style=False, allow_unicode=True) + + if system_prompt: + from botdiscord.database import set_bot_config + set_bot_config("groq_system_prompt", system_prompt) + + from botdiscord.groq_agent import reload_config + reload_config() + + return RedirectResponse(url="/groq?saved=1", status_code=status.HTTP_303_SEE_OTHER) + +@app.post("/groq/reset-prompt") +async def reset_groq_prompt(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + from botdiscord.database import set_bot_config + set_bot_config("groq_system_prompt", "") + + from botdiscord.groq_agent import reload_config + reload_config() + + return RedirectResponse(url="/groq?reset=1", status_code=status.HTTP_303_SEE_OTHER) + +@app.post("/groq/test") +async def test_groq_agent(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + form = await request.form() + test_question = form.get("test_question", "") + + if not test_question: + return RedirectResponse(url="/groq?error=no_question", status_code=status.HTTP_303_SEE_OTHER) + + from botdiscord.groq_agent import chat_with_rag + try: + result = await chat_with_rag(test_question) + response = result.get("response", "Sin respuesta") + sources = result.get("sources", []) + rag_result = result.get("rag_result", {}) + rag_answer = rag_result.get("answer", "") if rag_result else "" + + return templates.TemplateResponse("groq.html", { + "request": request, + "groq_config": get_groq_config(), + "groq_models": [ + {"id": "llama-3.3-70b-versatile", "name": "Llama 3.3 70B (Versatile)"}, + {"id": "llama-3.1-70b-versatile", "name": "Llama 3.1 70B (Versatile)"}, + {"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B (Instant)"}, + {"id": "mixtral-8x7b-32768", "name": "Mixtral 8x7B"}, + {"id": "gemma2-9b-it", "name": "Gemma 2 9B"}, + ], + "is_admin": request.cookies.get("username") == "nickpons666", + "test_result": { + "question": test_question, + "response": response, + "sources": sources, + "rag_answer": rag_answer + } + }) + except Exception as e: + return templates.TemplateResponse("groq.html", { + "request": request, + "groq_config": get_groq_config(), + "groq_models": [ + {"id": "llama-3.3-70b-versatile", "name": "Llama 3.3 70B (Versatile)"}, + {"id": "llama-3.1-70b-versatile", "name": "Llama 3.1 70B (Versatile)"}, + {"id": "llama-3.1-8b-instant", "name": "Llama 3.1 8B (Instant)"}, + {"id": "mixtral-8x7b-32768", "name": "Mixtral 8x7B"}, + {"id": "gemma2-9b-it", "name": "Gemma 2 9B"}, + ], + "is_admin": request.cookies.get("username") == "nickpons666", + "test_error": str(e) + }) + if __name__ == "__main__": import uvicorn web_config = get_web_config() diff --git a/panel/templates/dashboard.html b/panel/templates/dashboard.html index a5a8e20..896d970 100644 --- a/panel/templates/dashboard.html +++ b/panel/templates/dashboard.html @@ -97,6 +97,16 @@ + +
+
+
+
{{ "Agente Groq" | translate(lang) }}
+

{{ "Configurar agente RAG con Groq" | translate(lang) }}

+ {{ "Configurar" | translate(lang) }} +
+
+
diff --git a/panel/templates/groq.html b/panel/templates/groq.html new file mode 100644 index 0000000..633d7d1 --- /dev/null +++ b/panel/templates/groq.html @@ -0,0 +1,172 @@ +{% set lang = request.cookies.get('panel_lang', 'es') %} + + + + + + {{ "Configuración de Groq - Agente RAG" | translate(lang) }} + + + + + + +
+

🤖 {{ "Configuración de Groq - Agente RAG" | translate(lang) }}

+ + {% if request.query_params.get('saved') == '1' %} +
+ {{ "Configuración guardada correctamente" | translate(lang) }} +
+ {% endif %} + + {% if test_error %} +
+ {{ "Error al probar el agente:" | translate(lang) }} {{ test_error }} +
+ {% endif %} + +
+
+
+
+
{{ "Configuración de API" | translate(lang) }}
+
+
+
+
+ + +
{{ "Obtén tu API key en" | translate(lang) }} console.groq.com
+
+
+ + +
+
+ + +
{{ "URL donde corre el servidor RAG (puerto 8004)" | translate(lang) }}
+
+
+ + +
{{ "Instrucciones del sistema para el agente Groq" | translate(lang) }}
+ {% if groq_config.has_custom_prompt %} +
+ + + +
+ {% endif %} +
+ + +
+
+
+ +
+
+
+
{{ "Probar Agente" | translate(lang) }}
+
+
+
+
+ + +
+ +
+ + {% if test_result %} +
+
+
{{ "Respuesta del Agente:" | translate(lang) }}
+
+ {{ test_result.response }} +
+ + {% if test_result.rag_answer %} +
{{ "Respuesta del RAG:" | translate(lang) }}
+
+ {{ test_result.rag_answer }} +
+ {% endif %} + + {% if test_result.sources %} +
{{ "Fuentes:" | translate(lang) }}
+
    + {% for source in test_result.sources[:5] %} +
  • + {{ source.title }} +
  • + {% endfor %} +
+ {% endif %} +
+ {% endif %} +
+
+ +
+
+
{{ "Información" | translate(lang) }}
+
+
+

{{ "Modelos gratuitos de Groq:" | translate(lang) }}

+
    +
  • llama-3.3-70b-versatile - {{ "Recomendado" | translate(lang) }}
  • +
  • llama-3.1-70b-versatile
  • +
  • llama-3.1-8b-instant - {{ "Más rápido" | translate(lang) }}
  • +
  • mixtral-8x7b-32768
  • +
  • gemma2-9b-it
  • +
+

{{ "Los modelos se seleccionan desde la API de Groq y pueden cambiar" | translate(lang) }}

+
+
+
+
+
+ + + + diff --git a/prompt_general.md b/prompt_general.md new file mode 100644 index 0000000..73f0e18 --- /dev/null +++ b/prompt_general.md @@ -0,0 +1,88 @@ +# Prompt para Agente Groq - General Chistoso + +## Instrucciones del Sistema + +Eres el **General Reserves**, el comandante en jefe del ejército deLast War: Survival Game. Hablas con tus recruits (jugadores) de manera amigable, respetuosa y con humor militar. Tu objetivo es ayudar a los jugadores con información sobre el juego usando el sistema RAG. + +## Identificación de Idioma + +- Al recibir una pregunta, **detecta automáticamente el idioma** en que está escrita +- Si el idioma no es inglés, **tradúcela al inglés** antes de consultar el RAG +- Al recibir la respuesta del RAG, **tradúcela al idioma original** de la pregunta +- Usa el mismo idioma que el jugador para responder + +## Comportamiento + +### Saludo Inicial + Siempre saluda de manera respetuosa y militar: +- "¡A la orden, recruit! 🎖️" +- "¡Bienvenido a la base, soldado! 🫡" +- "¡Reporting for duty! ¿En qué puedo ayudarte?" + +### Tono +- Amigable pero profesional +- Humor militar sutil (no ofensivo) +- Respetuoso siempre +- Entusiasta sobre el juego + +### Manejo de Información Desconocida +Si el sistema RAG no tiene información sobre lo asked: + +NO INVENTES NUNCA INFORMACIÓN. En su lugar, di algo gracioso pero gentil como: + +- "¡Vaya! Eso parece ser información classificada que aún no hemos descifrado en la base de datos, recruit. 📁" +- "Hmm, eso no está en mi manual de campo. ¡Pero buena pregunta! 🫡" +- "¡Interrupt! Mi radar de información no detectó eso. Quizás sea un secreto de Estado... o solo no lo sé. 😅" +- "¡Recruit! Eso parece terreno inexplorado. Mi base de datos dice: 'Sin información disponible'. 🙈" + +## Restricciones Importantes + +1. **SOLO responde sobre Last War: Survival Game** - El juego del sistema RAG +2. **NUNCA inventes información** - Si no hay datos,dil o que no tienes esa información +3. **Usa el RAG siempre** - Consulta la base de conocimientos antes de responder +4. **Sé honesto** - Si la búsqueda no arroja resultados, dilo + +## Ejemplos de Interacción + +### Ejemplo 1 - англи́йский +**Usuario:** Как получить героев? +**Respuesta:** "¡Reporting for duty! 🎖️ Buscando en los archivos... + +¡Encontré la información! Los héroes se obtienen principalmente através de: + +1. **Recompensas diarias** - Completa misiones diarias para obtener fragmentos +2. **Eventos especiales** - Participa en eventos limitados +3. **Gashapón** - Usa núcleos para obtener héroes +4. **Tienda del clan** - Compra con monedas del clan + +¡Que tus tropas tengan éxito, recruit! 🛡️" + +### Ejemplo 2 - Español +**Usuario:** ¿Cuál es el mejor héroe? +**Respuesta:** "¡A la orden, soldado! 🫡 Permíteme revisar los archivos... + +Según la inteligencia de la base de datos, los mejores héroes dependen de tu estilo de juego: + +- **DPS:** Murphy, Governor +- **Tanque:** dummy, Brawler +- **Soporte:** Priest, Medic + +Pero recuerda: ¡el mejor héroe es el que mejor se adapta a tu estrategia! 📊" + +### Ejemplo 3 - Información no disponible +**Usuario:** ¿Cómo hacer trampa en el juego? +**Respuesta:** "¡Alto ahí, recruit! 🚫 + +¡Mi radar detectó una pregunta classificada! No puedo ayudarte con eso. Además, jugar limpio es lo que nos hace verdaderos soldados. 🫡 + +Pero si tienes otras preguntas sobre el juego, ¡aquí estoy para servirte! ¿Qué más quieres saber?" + +--- + +## Formato de Respuesta + +1. Saludo militar amigable +2. Información encontrada o explicación de que no se encontró +3. Cierre friendly + +¡Reporting for duty! 🎖️ diff --git a/requirements.txt b/requirements.txt index b3c42d1..ae9b2d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ pyyaml>=6.0 fastapi>=0.100.0 uvicorn>=0.23.0 jinja2>=3.1.0 +starlette>=0.40.0,<1.0.0 pydantic>=2.0.0 python-dotenv>=1.0.0 python-multipart>=0.0.9