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:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
209
botdiscord/groq_agent.py
Normal 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": []
|
||||
}
|
||||
Reference in New Issue
Block a user