Feat: Agregar agente Groq con integración RAG

- 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
This commit is contained in:
2026-03-26 21:23:19 -06:00
parent 48f7a80dc4
commit 8398e988b0
16 changed files with 1073 additions and 41 deletions

View File

@@ -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.

View File

@@ -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 . .

75
action_plan_groq_ui.md Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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()

209
botdiscord/groq_agent.py Normal file
View File

@@ -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": []
}

View File

@@ -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 <pregunta>\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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -97,6 +97,16 @@
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white mb-3" style="background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-robot"></i> {{ "Agente Groq" | translate(lang) }}</h5>
<p class="card-text">{{ "Configurar agente RAG con Groq" | translate(lang) }}</p>
<a href="/groq" class="btn btn-light btn-sm">{{ "Configurar" | translate(lang) }}</a>
</div>
</div>
</div>
</div>
<div class="card mt-4">

172
panel/templates/groq.html Normal file
View File

@@ -0,0 +1,172 @@
{% set lang = request.cookies.get('panel_lang', 'es') %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ "Configuración de Groq - Agente RAG" | translate(lang) }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
<i class="bi bi-translate"></i> {{ "Bots de Traducción" | translate(lang) }}
</a>
<div class="d-flex align-items-center">
<div class="dropdown me-3">
<button class="btn btn-outline-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-translate"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item {{ 'active' if lang == 'es' }}" href="/set-lang/es">Español</a></li>
<li><a class="dropdown-item {{ 'active' if lang == 'en' }}" href="/set-lang/en">English</a></li>
<li><a class="dropdown-item {{ 'active' if lang == 'pt' }}" href="/set-lang/pt">Português</a></li>
</ul>
</div>
<a href="/dashboard" class="btn btn-outline-light btn-sm me-2">{{ "Dashboard" | translate(lang) }}</a>
<a href="/logout" class="btn btn-outline-light btn-sm">{{ "Cerrar Sesión" | translate(lang) }}</a>
</div>
</div>
</nav>
<div class="container mt-5">
<h2 class="mb-4">🤖 {{ "Configuración de Groq - Agente RAG" | translate(lang) }}</h2>
{% if request.query_params.get('saved') == '1' %}
<div class="alert alert-success">
<i class="bi bi-check-circle"></i> {{ "Configuración guardada correctamente" | translate(lang) }}
</div>
{% endif %}
{% if test_error %}
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i> {{ "Error al probar el agente:" | translate(lang) }} {{ test_error }}
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-key"></i> {{ "Configuración de API" | translate(lang) }}</h5>
</div>
<div class="card-body">
<form method="post" action="/groq/save">
<div class="mb-3">
<label class="form-label">{{ "API Key de Groq" | translate(lang) }}</label>
<input type="password" class="form-control" name="api_key"
value="{{ groq_config.api_key if is_admin else '********************************' }}"
placeholder="sk-..." required>
<div class="form-text">{{ "Obtén tu API key en" | translate(lang) }} <a href="https://console.groq.com" target="_blank">console.groq.com</a></div>
</div>
<div class="mb-3">
<label class="form-label">{{ "Modelo" | translate(lang) }}</label>
<select class="form-select" name="model">
{% for model in groq_models %}
<option value="{{ model.id }}" {% if groq_config.model == model.id %}selected{% endif %}>
{{ model.name }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">{{ "URL del API RAG" | translate(lang) }}</label>
<input type="text" class="form-control" name="rag_url"
value="{{ groq_config.rag_url }}"
placeholder="http://localhost:8004">
<div class="form-text">{{ "URL donde corre el servidor RAG (puerto 8004)" | translate(lang) }}</div>
</div>
<div class="mb-3">
<label class="form-label">{{ "System Prompt" | translate(lang) }}</label>
<textarea class="form-control" name="system_prompt" rows="6">{{ groq_config.system_prompt }}</textarea>
<div class="form-text">{{ "Instrucciones del sistema para el agente Groq" | translate(lang) }}</div>
{% if groq_config.has_custom_prompt %}
<div class="mt-2">
<form method="post" action="/groq/reset-prompt" style="display:inline;">
<button type="submit" class="btn btn-warning btn-sm">
<i class="bi bi-arrow-counterclockwise"></i> {{ "Restablecer Prompt Default" | translate(lang) }}
</button>
</form>
</div>
{% endif %}
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> {{ "Guardar Configuración" | translate(lang) }}
</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-chat-dots"></i> {{ "Probar Agente" | translate(lang) }}</h5>
</div>
<div class="card-body">
<form method="post" action="/groq/test">
<div class="mb-3">
<label class="form-label">{{ "Pregunta de prueba" | translate(lang) }}</label>
<textarea class="form-control" name="test_question" rows="3"
placeholder="How to get heroes in Last War?">{{ test_result.question if test_result else '' }}</textarea>
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-send"></i> {{ "Enviar Pregunta" | translate(lang) }}
</button>
</form>
{% if test_result %}
<hr>
<div class="mt-3">
<h6>{{ "Respuesta del Agente:" | translate(lang) }}</h6>
<div class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;">
{{ test_result.response }}
</div>
{% if test_result.rag_answer %}
<h6 class="mt-3">{{ "Respuesta del RAG:" | translate(lang) }}</h6>
<div class="bg-info bg-opacity-10 p-3 rounded" style="max-height: 150px; overflow-y: auto;">
{{ test_result.rag_answer }}
</div>
{% endif %}
{% if test_result.sources %}
<h6 class="mt-3">{{ "Fuentes:" | translate(lang) }}</h6>
<ul class="list-group">
{% for source in test_result.sources[:5] %}
<li class="list-group-item">
<small>{{ source.title }}</small>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> {{ "Información" | translate(lang) }}</h5>
</div>
<div class="card-body">
<p><strong>{{ "Modelos gratuitos de Groq:" | translate(lang) }}</strong></p>
<ul>
<li><code>llama-3.3-70b-versatile</code> - {{ "Recomendado" | translate(lang) }}</li>
<li><code>llama-3.1-70b-versatile</code></li>
<li><code>llama-3.1-8b-instant</code> - {{ "Más rápido" | translate(lang) }}</li>
<li><code>mixtral-8x7b-32768</code></li>
<li><code>gemma2-9b-it</code></li>
</ul>
<p class="text-muted"><small>{{ "Los modelos se seleccionan desde la API de Groq y pueden cambiar" | translate(lang) }}</small></p>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

88
prompt_general.md Normal file
View File

@@ -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! 🎖️

View File

@@ -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