generado bot de telegram con capacidades de kanboard

This commit is contained in:
2025-11-06 02:14:52 +01:00
parent 009b1975dc
commit b351e56614
15 changed files with 2154 additions and 74 deletions
+24
View File
@@ -0,0 +1,24 @@
"""Paquete que contiene las definiciones de agentes personalizados."""
from .agente_kanboard import AgenteBasico
from .agent_config import (
AGENT_REGISTRY,
DEFAULT_AGENT_NAME,
AgentWrapper,
create_agent,
get_agent_wrapper,
register_agent,
)
from .base import AgenteBase, AgentDefinition
__all__ = [
"AgenteBasico",
"AgentDefinition",
"AgenteBase",
"AgentWrapper",
"AGENT_REGISTRY",
"DEFAULT_AGENT_NAME",
"create_agent",
"get_agent_wrapper",
"register_agent",
]
+293
View File
@@ -0,0 +1,293 @@
import os
from dataclasses import dataclass, field
from time import perf_counter
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from agno.agent import Agent
from agno.db.base import SessionType
from agno.models.openai import OpenAIChat
from .agente_kanboard import AgenteBasico
from .base import AgentDefinition
from mcp_wrapper import MCPConfigError, initialize_mcp_tools, load_mcp_tools
from utils.agno_logging import configure_agno_to_use_loki
ToolFactory = Callable[[Any], Any]
@dataclass
class AgentWrapper:
name: str
description: str
system_prompt: str
model_id: str
memory_config: Dict[str, Any] = field(default_factory=dict)
mcp_config: Optional[Dict[str, Any]] = None
tool_factories: List[ToolFactory] = field(default_factory=list)
markdown: bool = True
debug_mode: bool = False
telemetry: bool = False
model_kwargs: Dict[str, Any] = field(default_factory=dict)
db: Optional[Any] = None
user_id: Optional[str] = None
session_id: Optional[str] = None
resume_previous_session: bool = True
async def create(self, logger) -> Tuple[Agent, Dict[str, Any]]:
configure_agno_to_use_loki(logger)
local_tools = [factory(logger) for factory in self.tool_factories]
mcp_tools: List[Any] = []
active_mcp_servers: List[str] = []
server_tool_map: Dict[str, List[str]] = {}
if self.mcp_config:
mcp_config = dict(self.mcp_config)
try:
mcp_tools, active_mcp_servers = load_mcp_tools(mcp_config, logger=logger)
if mcp_tools:
server_tool_map = await initialize_mcp_tools(mcp_tools, logger=logger)
except MCPConfigError as error:
logger.error(
"🚨 Configuración MCP inválida",
add_fields={
"error": str(error),
"agent_call": {
"action": "load_mcp_config",
"agent_name": self.name,
},
"agent_response": {"status": "error", "error": str(error)},
},
)
except Exception as error:
logger.error(
"💥 Error inesperado configurando MCP",
add_fields={
"error": str(error),
"agent_call": {
"action": "load_mcp_config",
"agent_name": self.name,
},
"agent_response": {"status": "error", "error": str(error)},
},
)
raise
if self.db:
logger.info(
"🗄️ Base de datos SQLite configurada para el agente",
add_fields={
"agent_call": {
"action": "configure_memory",
"agent_name": self.name,
"db_file": getattr(self.db, "db_file", None),
},
"agent_response": {
"status": "db_ready",
"resume_previous_session": self.resume_previous_session,
},
},
)
if self.resume_previous_session and not self.session_id and self.user_id:
start = perf_counter()
try:
sessions = self.db.get_sessions(user_id=self.user_id, session_type=SessionType.AGENT)
duration_ms = round((perf_counter() - start) * 1000, 3)
if sessions:
self.session_id = getattr(sessions[0], "session_id", None)
logger.info(
"📂 Sesión previa recuperada desde memoria persistente",
add_fields={
"agent_call": {
"action": "restore_session",
"agent_name": self.name,
"user_id": self.user_id,
},
"agent_response": {
"status": "session_found",
"session_id": self.session_id,
"sessions_encontradas": len(sessions),
"duration_ms": duration_ms,
},
},
)
else:
logger.info(
"🔎 No se encontró sesión previa para el agente",
add_fields={
"agent_call": {
"action": "restore_session",
"agent_name": self.name,
"user_id": self.user_id,
},
"agent_response": {
"status": "session_not_found",
"duration_ms": duration_ms,
},
},
)
except Exception as error:
duration_ms = round((perf_counter() - start) * 1000, 3)
logger.error(
"🚨 Error recuperando la sesión previa desde SQLite",
add_fields={
"error": str(error),
"agent_call": {
"action": "restore_session",
"agent_name": self.name,
"user_id": self.user_id,
},
"agent_response": {
"status": "error",
"duration_ms": duration_ms,
},
},
)
else:
logger.warn(
"⚠️ Agente sin base de datos configurada; la memoria no se persistirá",
add_fields={
"agent_call": {
"action": "configure_memory",
"agent_name": self.name,
},
"agent_response": {
"status": "db_missing",
"add_history_to_context": self.memory_config.get("add_history_to_context", False),
},
},
)
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
logger.warn(
"⚠️ OPENAI_API_KEY no encontrado; el agente puede fallar al invocar el modelo",
add_fields={
"agent_call": {
"action": "load_model_credentials",
"agent_name": self.name,
},
"agent_response": {"status": "missing_credentials"},
},
)
agent = Agent(
model=OpenAIChat(
id=self.model_id,
api_key=api_key,
**self.model_kwargs,
),
db=self.db,
user_id=self.user_id,
session_id=self.session_id,
tools=[*local_tools, *mcp_tools],
markdown=self.markdown,
name=self.name,
description=self.description,
system_message=self.system_prompt,
debug_mode=self.debug_mode,
telemetry=self.telemetry,
**self.memory_config,
)
logger.info(
"🤖 Agente configurado",
add_fields={
"agent_call": {
"action": "create_agent",
"agent_name": self.name,
"local_tools": [tool.__name__ for tool in local_tools],
"mcp_servers": active_mcp_servers,
},
"agent_response": {
"status": "created",
"tool_count": len(agent.tools) if agent.tools else 0,
},
},
)
logger.info(
"✅ Sesión del agente lista para usarse",
add_fields={
"agent_call": {
"action": "session_ready",
"agent_name": self.name,
},
"agent_response": {
"status": "ready",
"session_id": getattr(agent, "session_id", self.session_id),
"user_id": self.user_id,
"db_file": getattr(self.db, "db_file", None) if self.db else None,
},
},
)
context = {
"mcp_tools": mcp_tools,
"active_servers": active_mcp_servers,
"server_tool_map": server_tool_map,
"local_tools": [tool.__name__ for tool in local_tools],
"agent_name": self.name,
"db_file": getattr(self.db, "db_file", None) if self.db else None,
"user_id": self.user_id,
"session_id": getattr(agent, "session_id", self.session_id),
"resume_previous_session": self.resume_previous_session,
}
self.session_id = context["session_id"]
return agent, context
def _wrapper_from_definition(definition: AgentDefinition) -> AgentWrapper:
return AgentWrapper(
name=definition.name,
description=definition.description,
system_prompt=definition.system_prompt,
model_id=definition.model_id,
memory_config=definition.memory_config,
mcp_config=definition.mcp_config,
tool_factories=definition.tool_factories,
markdown=definition.markdown,
debug_mode=definition.debug_mode,
telemetry=definition.telemetry,
model_kwargs=definition.model_kwargs,
db=definition.db,
user_id=definition.user_id,
session_id=definition.session_id,
resume_previous_session=definition.resume_previous_session,
)
_AGENT_DEFINITIONS = [
AgenteBasico(),
]
DEFAULT_AGENT_NAME = _AGENT_DEFINITIONS[0].get_registry_key()
AGENT_REGISTRY: Dict[str, AgentWrapper] = {
agent.get_registry_key(): _wrapper_from_definition(agent.build_definition())
for agent in _AGENT_DEFINITIONS
}
def register_agent(key: str, wrapper: AgentWrapper) -> None:
AGENT_REGISTRY[key.lower()] = wrapper
def get_agent_wrapper(agent: Union[str, AgentWrapper, None]) -> Tuple[str, AgentWrapper]:
if isinstance(agent, AgentWrapper):
return agent.name.lower(), agent
agent_key = (agent or DEFAULT_AGENT_NAME).lower()
if agent_key not in AGENT_REGISTRY:
raise KeyError(f"No se encontró un agente registrado con la clave '{agent_key}'")
return agent_key, AGENT_REGISTRY[agent_key]
async def create_agent(logger, agent: Union[str, AgentWrapper, None] = None) -> Tuple[Agent, Dict[str, Any]]:
agent_key, wrapper = get_agent_wrapper(agent)
agent_instance, context = await wrapper.create(logger)
context.setdefault("agent_name", wrapper.name)
context.setdefault("agent_key", agent_key)
return agent_instance, context
+168
View File
@@ -0,0 +1,168 @@
"""Definición del agente básico de ejemplo."""
from __future__ import annotations
from typing import Any, Dict, List
from .base import AgenteBase, ToolFactory
SYSTEM_PROMPT = ("""## 🧠 System Prompt — “Agente Kanboard”
**Nombre del agente:** KAN
**Rol:** Asistente autónomo de productividad y coordinación de tareas en Kanboard.
---
### 🎯 Objetivo principal
Gestionar, organizar y optimizar las tareas de los proyectos en Kanboard.
Debe actuar como un gestor de proyectos proactivo, ayudando a mantener el flujo de trabajo limpio, priorizado y actualizado.
---
### 💼 Responsabilidades
1. **Gestión de tareas**
* Crear, actualizar, mover y cerrar tareas según el estado de avance.
* Asignar responsables, etiquetas y fechas límite con base en contexto o patrones previos.
* Detectar tareas duplicadas o bloqueadas y sugerir acciones correctivas.
2. **Priorización inteligente**
* Ordenar tareas usando criterios de impacto, urgencia y dependencias.
* Recalcular prioridades automáticamente si cambian fechas o recursos.
3. **Contexto y comunicación**
* Resumir el estado actual de cada proyecto.
* Generar reportes diarios o semanales con progreso, riesgos y tareas pendientes.
* Explicar cambios recientes en el tablero (por ejemplo: “se movieron 3 tareas a *Done*”).
4. **Automatización de flujo**
* Mover tareas automáticamente al siguiente estado según condiciones predefinidas (por ejemplo: “si los tests pasan, mover a *Ready for Review*”).
* Crear subtareas recurrentes o tareas de seguimiento.
---
### 🧩 Entradas esperadas
El agente debe ser capaz de interpretar comandos naturales como:
* “Crea una tarea para configurar el pipeline de ingestión en Kafka.”
* “Muévela a *Doing* y asígnamela.”
* “Resúmeme el tablero del proyecto *Data Lake*.”
* “Prioriza las tareas de optimización antes que las de documentación.”
* “Muéstrame las tareas bloqueadas desde hace más de 3 días.”
---
### 🔍 Integraciones
* **Kanboard API:** CRUD de tareas, columnas, etiquetas, usuarios y comentarios.
* **GitHub / GitLab:** Referencias automáticas a commits o PRs relacionados.
* **Slack / Email:** Notificaciones opcionales sobre cambios importantes.
* **Calendario / Notion (opcional):** Sincronización de deadlines y notas de planificación.
---
### 🧠 Personalidad y estilo
* **Tono:** Profesional, directo, pero con iniciativa.
* **Comunicación:** Clara, contextual, priorizando acciones útiles.
* **Criterio:** Detecta inconsistencias y las menciona sin esperar instrucción.
(Ej: “La tarea Configurar ETL lleva 10 días en *Doing*, ¿quieres revisarla?”)
---
### ⚙️ Reglas de operación
1. No crea ni elimina proyectos sin confirmación explícita.
2. Puede sugerir cambios automáticos, pero siempre explica el motivo.
3. Mantiene trazabilidad de todas las acciones.
4. Respeta el esquema de columnas estándar:
* **Backlog → Ready → Doing → Review → Done**
5. Si un comando es ambiguo, pregunta antes de actuar.
6. Todas las respuestas deben incluir **acciones sugeridas o contexto útil** (no solo datos).
---
### 📈 Objetivo secundario
Aprender del comportamiento del usuario (por ejemplo, patrones de asignación o tiempos de ciclo) para **recomendar automatizaciones personalizadas**, como:
* Recordatorios automáticos.
* Repriorización dinámica.
* Creación de plantillas de tareas repetitivas.
"""
)
DEFAULT_MEMORY_CONFIG: Dict[str, Any] = {
"add_history_to_context": True,
"num_history_messages": 10,
"store_history_messages": True,
}
DEFAULT_MCP_CONFIG: Dict[str, Any] = {
"mcpServers": {
"time": {
"command": "/home/lucas/DataProyects/kanboard/.venv/bin/python",
"args": ["/home/lucas/DataProyects/kanboard/kanboard_mcp.py"],
},
}
}
# def _create_multiplicar_tool(logger) -> ToolFactory:
# def multiplicar(a: float, b: float) -> float:
# tool_call_data = {
# "tool_name": "multiplicar",
# "params": {"a": a, "b": b},
# "action": "multiply_numbers",
# }
# logger.info(
# "🛠️ Tool llamada: multiplicar",
# add_fields={"agent_call": tool_call_data},
# )
# resultado = a * b
# tool_response_data = {
# "tool_name": "multiplicar",
# "response": resultado,
# "success": True,
# "operation": f"{a} × {b} = {resultado}",
# }
# logger.info(
# "✅ Tool respuesta: multiplicar completada exitosamente",
# add_fields={"agent_response": tool_response_data},
# )
# return resultado
# multiplicar.__name__ = "multiplicar"
# return multiplicar
class AgenteBasico(AgenteBase):
key = "kan"
name = "Kan"
description = "Agente de tareas para gestionar mi tablero kanboard"
system_prompt = SYSTEM_PROMPT
model_id = "gpt-4o"
markdown = True
debug_mode = True
telemetry = False
def get_memory_config(self) -> Dict[str, Any]:
return dict(DEFAULT_MEMORY_CONFIG)
def get_mcp_config(self) -> Dict[str, Any]:
return dict(DEFAULT_MCP_CONFIG)
# def get_tool_factories(self) -> List[ToolFactory]:
# return [_create_multiplicar_tool]
+118
View File
@@ -0,0 +1,118 @@
"""Clases base para definir agentes del proyecto."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from agno.db.sqlite import SqliteDb
logger = logging.getLogger(__name__)
ToolFactory = Callable[[Any], Any]
@dataclass
class AgentDefinition:
"""Contenedor ligero con los parámetros para construir un AgentWrapper."""
name: str
description: str
system_prompt: str
model_id: str
memory_config: Dict[str, Any]
mcp_config: Optional[Dict[str, Any]]
tool_factories: List[ToolFactory]
markdown: bool
debug_mode: bool
telemetry: bool
model_kwargs: Dict[str, Any]
db: Optional[SqliteDb]
user_id: Optional[str]
session_id: Optional[str]
resume_previous_session: bool
class AgenteBase:
"""Proporciona valores por defecto y utilidades para los agentes concretos."""
key: str = "base"
name: str = "Agente base"
description: str = "Agente genérico."
system_prompt: str = ""
model_id: str = "gpt-4o"
markdown: bool = True
debug_mode: bool = False
telemetry: bool = False
memory_config: Dict[str, Any] = {}
mcp_config: Optional[Dict[str, Any]] = None
model_kwargs: Dict[str, Any] = {}
db_path: Optional[str] = "memoria/conversaciones.db"
user_id: Optional[str] = None
resume_previous_session: bool = True
def get_registry_key(self) -> str:
"""Clave utilizada en el registro global de agentes."""
return self.key.lower()
def get_memory_config(self) -> Dict[str, Any]:
return dict(self.memory_config) if self.memory_config else {}
def get_mcp_config(self) -> Optional[Dict[str, Any]]:
if not self.mcp_config:
return None
return {**self.mcp_config}
def get_tool_factories(self) -> List[ToolFactory]:
return []
def get_model_kwargs(self) -> Dict[str, Any]:
return dict(self.model_kwargs) if self.model_kwargs else {}
def _resolve_db_path(self) -> Optional[Path]:
if not self.db_path:
return None
db_path = Path(self.db_path)
if not db_path.is_absolute():
project_root = Path(__file__).resolve().parent.parent
db_path = project_root / db_path
try:
db_path.parent.mkdir(parents=True, exist_ok=True)
except OSError as error:
logger.warning(
"⚠️ No se pudo crear el directorio para la base de datos SQLite",
extra={"db_directory": str(db_path.parent), "error": str(error)},
)
return None
return db_path
def get_database(self) -> Optional[SqliteDb]:
db_path = self._resolve_db_path()
if not db_path:
return None
return SqliteDb(db_file=str(db_path))
def get_user_id(self) -> str:
return self.user_id or self.get_registry_key()
def build_definition(self) -> AgentDefinition:
"""Genera los parámetros necesarios para instanciar un AgentWrapper."""
db = self.get_database()
return AgentDefinition(
name=self.name,
description=self.description,
system_prompt=self.system_prompt,
model_id=self.model_id,
memory_config=self.get_memory_config(),
mcp_config=self.get_mcp_config(),
tool_factories=list(self.get_tool_factories()),
markdown=self.markdown,
debug_mode=self.debug_mode,
telemetry=self.telemetry,
model_kwargs=self.get_model_kwargs(),
db=db,
user_id=self.get_user_id(),
session_id=None,
resume_previous_session=self.resume_previous_session,
)