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

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