diff --git a/orchestrator/providers/base.py b/orchestrator/providers/base.py index bc81d6c..7d27533 100644 --- a/orchestrator/providers/base.py +++ b/orchestrator/providers/base.py @@ -6,8 +6,11 @@ from dataclasses import dataclass, field from typing import Optional, Any from datetime import datetime import asyncio -import time -from collections import deque + +try: + from ..utils import RateLimiter +except ImportError: + from utils import RateLimiter @dataclass @@ -31,29 +34,6 @@ class ProviderResponse: 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): """ Clase base abstracta para todos los providers de modelos. diff --git a/orchestrator/tools/executor.py b/orchestrator/tools/executor.py index 3bfea04..11c3fdc 100644 --- a/orchestrator/tools/executor.py +++ b/orchestrator/tools/executor.py @@ -20,7 +20,11 @@ from pathlib import Path from dataclasses import dataclass, field from typing import Optional, Any, Callable from datetime import datetime -from collections import deque + +try: + from ..utils import RateLimiter +except ImportError: + from utils import RateLimiter @dataclass @@ -35,31 +39,6 @@ class ToolResult: 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: """Validador de seguridad para herramientas.""" diff --git a/orchestrator/utils.py b/orchestrator/utils.py new file mode 100644 index 0000000..4be1eb8 --- /dev/null +++ b/orchestrator/utils.py @@ -0,0 +1,68 @@ +# 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 time +from collections import deque + + +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)