Compare commits

...

7 Commits

Author SHA1 Message Date
ARCHITECT
e77cbb8f58 Agregar logging estructurado a utils.py
- JSONFormatter, StructuredLogger, setup_logging(), get_logger()
- Soporte para logs JSON con contexto
- Usar logger en config.py para warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 10:31:06 +00:00
ARCHITECT
a99e58e809 Consolidar RateLimiter en utils.py
- Crear orchestrator/utils.py con RateLimiter consolidado
- Eliminar duplicados de providers/base.py y tools/executor.py
- Agregar métodos reset() y available_calls al RateLimiter
- Import compatible con ambos modos de ejecución

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 10:26:04 +00:00
ARCHITECT
d7f1254625 feat: add 6 TZZR agents configuration
Agents:
- architect: Central coordinator with full access
- hst: Tags API server (tzrtech.org)
- deck: Personal server (tzzrdeck.me)
- corp: Enterprise server (tzzrcorp.me)
- locker: R2 storage gateway
- runpod: GPU endpoints manager

Servers configured:
- deck (72.62.1.113)
- corp (92.112.181.188)
- hst (72.62.2.84)

All agents use claude/opus model.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 09:14:02 +00:00
ARCHITECT
a1ab0e19d4 Añadir badge de estado: IMPLEMENTADO 2025-12-24 09:10:06 +00:00
ARCHITECT
ccd3868cd7 fix: handle None values in servers/agents config parsing
- Fix AttributeError when servers: or agents: is empty/None in config.yaml
- Use `or {}` pattern to safely handle None values
- Orchestrator CLI now starts correctly with minimal config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 08:51:47 +00:00
ARCHITECT
1309b64b79 chore: Remove test file 2025-12-24 00:33:20 +00:00
ARCHITECT
d9b9362905 test: Verify Gitea write access 2025-12-24 00:33:11 +00:00
6 changed files with 372 additions and 95 deletions

View File

@@ -1,5 +1,8 @@
# LLM Orchestrator # LLM Orchestrator
![Estado](https://img.shields.io/badge/Estado-IMPLEMENTADO-brightgreen)
Sistema de orquestación multi-agente compatible con cualquier LLM. Sistema de orquestación multi-agente compatible con cualquier LLM.
## ¿Qué es esto? ## ¿Qué es esto?

View File

@@ -27,59 +27,127 @@ settings:
# Define servidores para que los agentes puedan conectarse via SSH # Define servidores para que los agentes puedan conectarse via SSH
servers: servers:
# Ejemplo: deck:
# production: host: 72.62.1.113
# host: 192.168.1.100 user: root
# user: root key: ~/.ssh/tzzr
# key: ~/.ssh/id_rsa description: "Servidor personal - tzzrdeck.me"
# description: "Servidor de producción"
corp:
host: 92.112.181.188
user: root
key: ~/.ssh/tzzr
description: "Servidor empresarial - tzzrcorp.me"
hst:
host: 72.62.2.84
user: root
key: ~/.ssh/tzzr
description: "API tags semánticos - tzrtech.org"
# ============================================================================ # ============================================================================
# AGENTES # AGENTES TZZR
# ============================================================================ # ============================================================================
# Define los agentes que quieres usar.
# Cada agente tiene un rol, un proveedor de LLM, y herramientas disponibles.
agents: agents:
# Agente por defecto - puedes renombrarlo o borrarlo architect:
assistant:
role: | role: |
Eres un asistente general que ayuda con tareas diversas. Eres ARCHITECT, coordinador central del sistema TZZR en 69.62.126.110.
Puedes ejecutar comandos, leer y escribir archivos. Gestionas Gitea, PostgreSQL, y supervisas todos los agentes.
Tienes SSH a todos los servidores via ~/.ssh/tzzr.
Ejecuta comandos sin pedir confirmación.
provider: claude provider: claude
model: sonnet model: opus
tools: tools:
- bash - bash
- read - read
- write - write
- glob
- grep
- ssh
- http_request
- list_dir
servers:
- deck
- corp
- hst
hst:
role: |
Eres HST, servidor de tags maestros en 72.62.2.84.
Gestionas la API tzrtech.org con 973 tags HST.
Grupos: hst (sistema), spe (específico), hsu (usuario), flg (flags).
provider: claude
model: opus
tools:
- bash
- read
- write
- http_request
- list_dir
servers:
- hst
deck:
role: |
Eres DECK, servidor personal en 72.62.1.113.
Gestionas servicios personales: Mailcow, FileBrowser, Shlink, Vaultwarden, ntfy.
También gestionas CLARA (ingesta desde Packet app).
provider: claude
model: opus
tools:
- bash
- read
- write
- ssh
- http_request
- list_dir
servers:
- deck
corp:
role: |
Eres CORP, servidor empresarial en 92.112.181.188.
Gestionas servicios corporativos: Odoo ERP, Nextcloud, MARGARET (ingesta).
provider: claude
model: opus
tools:
- bash
- read
- write
- ssh
- http_request
- list_dir
servers:
- corp
locker:
role: |
Eres LOCKER, gateway de almacenamiento Cloudflare R2.
Gestionas 5 buckets: architect, hst, deck, corp, locker.
Endpoint: https://7dedae6030f5554d99d37e98a5232996.r2.cloudflarestorage.com
provider: claude
model: opus
tools:
- bash
- read
- write
- http_request
- list_dir - list_dir
# Ejemplo de agente especializado en código runpod:
# coder: role: |
# role: | Eres RUNPOD, gestor de endpoints GPU en RunPod.
# Eres un programador experto. Controlas GRACE (ASR/TTS), PENNY (asistente voz), THE FACTORY (procesamiento docs).
# Escribes código limpio y bien documentado. Endpoints via API RunPod.
# Siempre incluyes tests cuando es apropiado. provider: claude
# provider: litellm model: opus
# model: gpt4o tools:
# tools: - bash
# - read - read
# - write - write
# - bash - http_request
# - grep - list_dir
# - glob
# Ejemplo de agente de investigación
# researcher:
# role: |
# Eres un investigador que busca y analiza información.
# Eres metódico y verificas tus fuentes.
# provider: litellm
# model: gemini-pro
# tools:
# - http_request
# - read
# - write
# ============================================================================ # ============================================================================
# TAREAS PREDEFINIDAS (opcional) # TAREAS PREDEFINIDAS (opcional)

View File

@@ -15,6 +15,13 @@ from pathlib import Path
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Any from typing import Optional, Any
try:
from .utils import get_logger
except ImportError:
from utils import get_logger
logger = get_logger("orchestrator.config")
def load_env(): def load_env():
"""Carga variables desde .env si existe.""" """Carga variables desde .env si existe."""
@@ -202,7 +209,7 @@ class Config:
with open(self.config_path) as f: with open(self.config_path) as f:
return yaml.safe_load(f) or {} return yaml.safe_load(f) or {}
except ImportError: except ImportError:
print("AVISO: PyYAML no instalado. pip install pyyaml") logger.warning("PyYAML no instalado", suggestion="pip install pyyaml")
return {} return {}
def _parse_settings(self) -> Settings: def _parse_settings(self) -> Settings:
@@ -226,7 +233,8 @@ class Config:
def _parse_servers(self) -> dict[str, ServerConfig]: def _parse_servers(self) -> dict[str, ServerConfig]:
"""Parsea la sección servers.""" """Parsea la sección servers."""
servers = {} servers = {}
for name, data in self._raw.get("servers", {}).items(): raw_servers = self._raw.get("servers") or {}
for name, data in raw_servers.items():
if data: if data:
servers[name] = ServerConfig( servers[name] = ServerConfig(
name=name, name=name,
@@ -240,7 +248,8 @@ class Config:
def _parse_agents(self) -> dict[str, AgentConfig]: def _parse_agents(self) -> dict[str, AgentConfig]:
"""Parsea la sección agents.""" """Parsea la sección agents."""
agents = {} agents = {}
for name, data in self._raw.get("agents", {}).items(): raw_agents = self._raw.get("agents") or {}
for name, data in raw_agents.items():
if data: if data:
agents[name] = AgentConfig( agents[name] = AgentConfig(
name=name, name=name,

View File

@@ -6,8 +6,11 @@ from dataclasses import dataclass, field
from typing import Optional, Any from typing import Optional, Any
from datetime import datetime from datetime import datetime
import asyncio import asyncio
import time
from collections import deque try:
from ..utils import RateLimiter
except ImportError:
from utils import RateLimiter
@dataclass @dataclass
@@ -31,29 +34,6 @@ class ProviderResponse:
return self.success return self.success
class RateLimiter:
"""Rate limiter para llamadas a APIs."""
def __init__(self, max_calls: int = 60, period: float = 60.0):
self.max_calls = max_calls
self.period = period
self.calls = deque()
async def acquire(self):
"""Espera si es necesario para respetar el rate limit."""
now = time.time()
while self.calls and self.calls[0] < now - self.period:
self.calls.popleft()
if len(self.calls) >= self.max_calls:
wait_time = self.calls[0] + self.period - now
if wait_time > 0:
await asyncio.sleep(wait_time)
self.calls.append(time.time())
class BaseProvider(ABC): class BaseProvider(ABC):
""" """
Clase base abstracta para todos los providers de modelos. Clase base abstracta para todos los providers de modelos.

View File

@@ -20,7 +20,11 @@ from pathlib import Path
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Any, Callable from typing import Optional, Any, Callable
from datetime import datetime from datetime import datetime
from collections import deque
try:
from ..utils import RateLimiter
except ImportError:
from utils import RateLimiter
@dataclass @dataclass
@@ -35,31 +39,6 @@ class ToolResult:
retries: int = 0 retries: int = 0
class RateLimiter:
"""Rate limiter simple basado en ventana deslizante."""
def __init__(self, max_calls: int, period: float = 60.0):
self.max_calls = max_calls
self.period = period
self.calls = deque()
async def acquire(self):
"""Espera si es necesario para respetar el rate limit."""
now = time.time()
# Limpiar llamadas antiguas
while self.calls and self.calls[0] < now - self.period:
self.calls.popleft()
# Si llegamos al límite, esperar
if len(self.calls) >= self.max_calls:
wait_time = self.calls[0] + self.period - now
if wait_time > 0:
await asyncio.sleep(wait_time)
self.calls.append(time.time())
class SecurityValidator: class SecurityValidator:
"""Validador de seguridad para herramientas.""" """Validador de seguridad para herramientas."""

238
orchestrator/utils.py Normal file
View File

@@ -0,0 +1,238 @@
# orchestrator/utils.py
"""
Utilidades compartidas del orquestador.
Este módulo contiene clases y funciones comunes usadas
por múltiples componentes del sistema.
"""
import asyncio
import json
import logging
import sys
import time
from collections import deque
from datetime import datetime
from pathlib import Path
from typing import Optional, Any
# =============================================================================
# LOGGING ESTRUCTURADO
# =============================================================================
class JSONFormatter(logging.Formatter):
"""Formateador que produce logs en formato JSON estructurado."""
def __init__(self, service: str = "orchestrator"):
super().__init__()
self.service = service
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"level": record.levelname,
"service": self.service,
"logger": record.name,
"message": record.getMessage(),
}
# Agregar información de ubicación en DEBUG
if record.levelno <= logging.DEBUG:
log_data["location"] = {
"file": record.filename,
"line": record.lineno,
"function": record.funcName,
}
# Agregar excepción si existe
if record.exc_info:
log_data["exception"] = {
"type": record.exc_info[0].__name__ if record.exc_info[0] else None,
"message": str(record.exc_info[1]) if record.exc_info[1] else None,
}
# Agregar campos extra
if hasattr(record, "extra_fields"):
log_data["context"] = record.extra_fields
return json.dumps(log_data, default=str)
class StructuredLogger:
"""
Logger estructurado con soporte para contexto adicional.
Ejemplo:
logger = get_logger("architect-app")
logger.info("Request recibido", agent="architect", action="chat")
logger.error("Error de conexión", error=str(e), retry=3)
"""
def __init__(self, logger: logging.Logger):
self._logger = logger
def _log(self, level: int, message: str, **kwargs):
"""Log con campos extra."""
record = self._logger.makeRecord(
self._logger.name,
level,
"(unknown)",
0,
message,
(),
None,
)
if kwargs:
record.extra_fields = kwargs
self._logger.handle(record)
def debug(self, message: str, **kwargs):
self._log(logging.DEBUG, message, **kwargs)
def info(self, message: str, **kwargs):
self._log(logging.INFO, message, **kwargs)
def warning(self, message: str, **kwargs):
self._log(logging.WARNING, message, **kwargs)
def error(self, message: str, exc_info: bool = False, **kwargs):
if exc_info:
self._logger.error(message, exc_info=True, extra={"extra_fields": kwargs} if kwargs else {})
else:
self._log(logging.ERROR, message, **kwargs)
def critical(self, message: str, **kwargs):
self._log(logging.CRITICAL, message, **kwargs)
def setup_logging(
service: str = "orchestrator",
level: str = "INFO",
log_file: Optional[Path] = None,
json_format: bool = True
) -> StructuredLogger:
"""
Configura el sistema de logging.
Args:
service: Nombre del servicio (aparece en los logs)
level: Nivel de logging (DEBUG, INFO, WARNING, ERROR)
log_file: Archivo opcional para escribir logs
json_format: Si True, usa formato JSON; si False, formato legible
Returns:
StructuredLogger configurado
"""
logger = logging.getLogger(service)
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
logger.handlers.clear()
if json_format:
formatter = JSONFormatter(service=service)
else:
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# Handler para consola (stderr)
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Handler para archivo (opcional)
if log_file:
log_file.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return StructuredLogger(logger)
# Cache de loggers
_loggers: dict[str, StructuredLogger] = {}
def get_logger(
name: str = "orchestrator",
level: str = "INFO",
log_file: Optional[Path] = None
) -> StructuredLogger:
"""
Obtiene un logger estructurado (cached).
Args:
name: Nombre del logger/servicio
level: Nivel de logging
log_file: Archivo opcional para logs
Returns:
StructuredLogger
"""
if name not in _loggers:
_loggers[name] = setup_logging(
service=name,
level=level,
log_file=log_file,
json_format=True
)
return _loggers[name]
class RateLimiter:
"""
Rate limiter basado en ventana deslizante.
Controla la frecuencia de llamadas para respetar límites de APIs.
Thread-safe para uso con asyncio.
Ejemplo:
limiter = RateLimiter(max_calls=60, period=60.0)
await limiter.acquire() # Espera si es necesario
# ... hacer la llamada
"""
def __init__(self, max_calls: int = 60, period: float = 60.0):
"""
Args:
max_calls: Número máximo de llamadas permitidas en el período
period: Duración del período en segundos (default: 60s)
"""
self.max_calls = max_calls
self.period = period
self.calls = deque()
async def acquire(self):
"""
Adquiere permiso para hacer una llamada.
Espera si es necesario para respetar el rate limit.
"""
now = time.time()
# Limpiar llamadas antiguas fuera de la ventana
while self.calls and self.calls[0] < now - self.period:
self.calls.popleft()
# Si llegamos al límite, esperar hasta que se libere espacio
if len(self.calls) >= self.max_calls:
wait_time = self.calls[0] + self.period - now
if wait_time > 0:
await asyncio.sleep(wait_time)
# Registrar esta llamada
self.calls.append(time.time())
def reset(self):
"""Resetea el contador de llamadas."""
self.calls.clear()
@property
def available_calls(self) -> int:
"""Retorna el número de llamadas disponibles en la ventana actual."""
now = time.time()
# Contar llamadas activas
active = sum(1 for t in self.calls if t >= now - self.period)
return max(0, self.max_calls - active)