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

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