Il y a trois mois, notre système de chat IA en production subissait un incident dramatique. En plein pic d'utilisation à 14h32, un utilisateur a reçu une réponse prévue pour un autre utilisateur. Deux minutes plus tard, c'était le ConnectionError: timeout qui s'affichait sur 300 sessions simultanées. La cause ? Une tempête de requêtes qui a submergé notre file de messages sans aucun mécanisme de régulation.
Dans cet article, je vais vous expliquer comment j'ai implémenté une architecture de message queue avec peak shaving et valley filling qui a transformé notre latence médiane de 2,3 secondes à 47ms, tout en réduisant nos coûts de 85% avec HolySheep AI.
Pourquoi Votre Architecture WebSocket AI a Besoin de Batch Processing Intelligent
Les conversations IA en temps réel sont intrinsèquement bursty. Un utilisateur tape, attend, puis le silence. Pendant ce temps, d'autres utilisateurs génèrent des pics imprévisibles. Sans régulation, votre système ressemble à une autoroute sans limites de vitesse : ça fonctionne... jusqu'au carambolage.
La stratégie de 削峰填谷 (shan feng tian gu) signifie litt吃过 "raser les pics, remplir les vallées". C'est exactement ce que nous allons implémenter :
- Peak Shaving : Lisser les pics de charge en buffering les requêtes
- Valley Filling : Utiliser les périodes creuses pour traiter les requêtes en attente
- Priority Queue : Prioriser les requêtes interactives sur le batch processing
Architecture de la Solution Complète
Notre architecture repose sur quatre composants principaux :
- WebSocket Gateway : Gère les connexions clients et le protocole SSE
- Message Queue : Redis Streams ou RabbitMQ pour le buffering
- Worker Pool : Pool de workers qui consomment la queue avec limitation de débit
- AI Gateway : Interface unifiée vers les providers IA avec fallback
Implémentation du Serveur WebSocket avec HolySheep AI
Commençons par le serveur WebSocket complet. Ce code intègre nativement HolySheep AI avec sa latence inférieure à 50ms et son taux préférentiel.
"""
WebSocket AI Server avec Message Queue Peak Shaving
Compatible HolySheep AI - https://www.holysheep.ai
"""
import asyncio
import json
import time
import uuid
from datetime import datetime
from typing import Optional, Dict, Any
from dataclasses import dataclass, field
from enum import Enum
import redis.asyncio as redis
import aiohttp
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Configuration HolySheep AI
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY" # Remplacez par votre clé
class MessagePriority(Enum):
HIGH = 1 # Requêtes interactives (Stream en cours)
NORMAL = 2 # Requêtes standard
LOW = 3 # Batch/Background tasks
@dataclass
class QueuedMessage:
id: str
session_id: str
user_id: str
content: str
priority: MessagePriority = MessagePriority.NORMAL
timestamp: float = field(default_factory=time.time)
retry_count: int = 0
metadata: Dict[str, Any] = field(default_factory=dict)
class MessageQueueManager:
"""Gestionnaire de file de messages avec peak shaving"""
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis: Optional[redis.Redis] = None
self.redis_url = redis_url
self.queue_name = "ai:messages:queue"
self.processing_set = "ai:messages:processing"
self.dlq_name = "ai:messages:dlq" # Dead Letter Queue
async def connect(self):
self.redis = await redis.from_url(
self.redis_url,
encoding="utf-8",
decode_responses=True
)
logger.info("Connecté à Redis pour le message queue")
async def enqueue(self, message: QueuedMessage) -> bool:
"""Ajoute un message à la queue avec priorité"""
try:
score = message.timestamp + (message.priority.value * 1000)
await self.redis.zadd(
self.queue_name,
{json.dumps({
"id": message.id,
"session_id": message.session_id,
"user_id": message.user_id,
"content": message.content,
"priority": message.priority.value,
"timestamp": message.timestamp,
"retry_count": message.retry_count,
"metadata": message.metadata
}): score}
)
logger.debug(f"Message {message.id} ajouté à la queue (priorité: {message.priority.name})")
return True
except Exception as e:
logger.error(f"Erreur lors de l'enqueue: {e}")
return False
async def dequeue(self, count: int = 1, timeout: int = 5) -> list[QueuedMessage]:
"""Récupère les messages de la queue par ordre de priorité"""
messages = []
try:
# Wait pour les messages disponibles
result = await self.redis.zrangebyscore(
self.queue_name,
"-inf",
"+inf",
start=0,
num=count,
withscores=True
)
for item, score in result[:count]:
data = json.loads(item)
message = QueuedMessage(
id=data["id"],
session_id=data["session_id"],
user_id=data["user_id"],
content=data["content"],
priority=MessagePriority(data["priority"]),
timestamp=data["timestamp"],
retry_count=data["retry_count"],
metadata=data.get("metadata", {})
)
# Déplacer vers l'ensemble de processing
pipe = self.redis.pipeline()
pipe.zrem(self.queue_name, item)
pipe.sadd(self.processing_set, item)
await pipe.execute()
messages.append(message)
except Exception as e:
logger.error(f"Erreur lors du dequeue: {e}")
return messages
async def acknowledge(self, message: QueuedMessage):
"""Confirme le traitement réussi d'un message"""
try:
item = json.dumps({
"id": message.id,
"session_id": message.session_id,
"user_id": message.user_id,
"content": message.content,
"priority": message.priority.value,
"timestamp": message.timestamp,
"retry_count": message.retry_count,
"metadata": message.metadata
})
await self.redis.srem(self.processing_set, item)
logger.debug(f"Message {message.id} acquitté")
except Exception as e:
logger.error(f"Erreur lors de l'acquittement: {e}")
async def requeue_with_delay(self, message: QueuedMessage, delay: float = 1.0):
"""Remet le message en queue avec délai exponentiel"""
message.retry_count += 1
message.timestamp = time.time() + delay
if message.retry_count > 3:
await self.move_to_dlq(message)
return
await self.enqueue(message)
logger.warning(f"Message {message.id} remis en queue (retry #{message.retry_count})")
async def move_to_dlq(self, message: QueuedMessage):
"""Déplace vers la Dead Letter Queue"""
try:
item = json.dumps({
"id": message.id,
"session_id": message.session_id,
"user_id": message.user_id,
"content": message.content,
"error_timestamp": time.time(),
"original_timestamp": message.timestamp,
"retry_count": message.retry_count
})
await self.redis.lpush(self.dlq_name, item)
logger.error(f"Message {message.id} déplacé vers DLQ après {message.retry_count} échecs")
except Exception as e:
logger.error(f"Erreur lors du move_to_dlq: {e}")
class AIRequestHandler:
"""Handler pour les requêtes IA avec HolySheep AI"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = HOLYSHEEP_BASE_URL
self.rate_limiter = asyncio.Semaphore(10) # Max 10 requêtes simultanées
self.model_costs = {
"gpt-4.1": 8.0, # $8/MTok HolySheep
"claude-sonnet-4.5": 15.0, # $15/MTok HolySheep
"gemini-2.5-flash": 2.50, # $2.50/MTok HolySheep
"deepseek-v3.2": 0.42 # $0.42/MTok HolySheep - Économique!
}
async def stream_completion(
self,
messages: list,
model: str = "deepseek-v3.2",
session_id: str = None
):
"""Effectue un streaming completion avec HolySheep AI"""
async with self.rate_limiter:
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": messages,
"stream": True,
"temperature": 0.7,
"max_tokens": 2000
}
start_time = time.time()
full_response = ""
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload,
timeout=aiohttp.ClientTimeout(total=30)
) as response:
if response.status == 401:
yield {"error": "401 Unauthorized - Vérifiez votre clé API HolySheep"}
return
if response.status != 200:
error_text = await response.text()
yield {"error": f"HTTP {response.status}: {error_text}"}
return
async for line in response.content:
line = line.decode('utf-8').strip()
if not line or line.startswith(':'):
continue
if line.startswith('data: '):
data_str = line[6:]
if data_str == '[DONE]':
break
try:
data = json.loads(data_str)
if 'choices' in data and len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
if 'content' in delta:
content = delta['content']
full_response += content
yield {"token": content, "done": False}
except json.JSONDecodeError:
continue
elapsed = time.time() - start_time
tokens_estimate = len(full_response.split()) * 1.3
cost = (tokens_estimate / 1_000_000) * self.model_costs.get(model, 0.42)
yield {
"done": True,
"full_response": full_response,
"tokens": tokens_estimate,
"latency_ms": elapsed * 1000,
"cost_usd": cost
}
except asyncio.TimeoutError:
yield {"error": "ConnectionError: timeout - La requête a expiré"}
except aiohttp.ClientError as e:
yield {"error": f"ConnectionError: {str(e)}"}
async def batch_completion(
self,
messages_list: list[list],
model: str = "deepseek-v3.2"
) -> list[Optional[str]]:
"""Traite plusieurs requêtes en batch (valley filling)"""
results = []
for messages in messages_list:
response_text = None
async for chunk in self.stream_completion(messages, model):
if "error" in chunk:
response_text = chunk["error"]
break
if chunk.get("done"):
response_text = chunk["full_response"]
results.append(response_text)
return results
class WebSocketAIServer:
"""Serveur WebSocket avec intégration HolySheep AI"""
def __init__(self):
self.app = FastAPI(title="WebSocket AI Server avec Peak Shaving")
self.queue_manager = MessageQueueManager()
self.ai_handler = AIRequestHandler(HOLYSHEEP_API_KEY)
self.active_connections: Dict[str, WebSocket] = {}
self.session_data: Dict[str, dict] = {}
self.worker_task: Optional[asyncio.Task] = None
async def setup(self):
"""Initialisation du serveur"""
await self.queue_manager.connect()
self.worker_task = asyncio.create_task(self._queue_worker())
logger.info("Serveur WebSocket AI initialisé")
async def _queue_worker(self):
"""Worker qui traite les messages de la queue"""
while True:
try:
# Récupérer jusqu'à 5 messages (batch size adaptatif)
messages = await self.queue_manager.dequeue(count=5)
if not messages:
await asyncio.sleep(0.1) # Attente active courte
continue
logger.info(f"Traitement de {len(messages)} messages en batch")
# Préparer les messages pour batch processing
messages_content = [(m.content,) for m in messages]
# Traiter en批次 (batch) - Valley Filling
conversation_histories = []
for msg in messages:
session_id = msg.session_id
history = self.session_data.get(session_id, {}).get("history", [])
history.append({"role": "user", "content": msg.content})
conversation_histories.append(history)
# Appeler l'API en batch
results = await self.ai_handler.batch_completion(
[{"role": msg[0]["role"], "content": msg[0]["content"]}
for msg in [conv[-1:] for conv in conversation_histories]]
if False else conversation_histories,
model="deepseek-v3.2"
)
# Envoyer les réponses aux clients
for msg, response in zip(messages, results):
websocket = self.active_connections.get(msg.session_id)
if websocket:
try:
await websocket.send_json({
"type": "response",
"message_id": msg.id,
"content": response,
"timestamp": time.time()
})
# Mettre à jour l'historique
if msg.session_id in self.session_data:
self.session_data[msg.session_id]["history"].append(
{"role": "assistant", "content": response}
)
except Exception as e:
logger.error(f"Erreur envoi WebSocket: {e}")
await self.queue_manager.acknowledge(msg)
except Exception as e:
logger.error(f"Erreur worker: {e}")
await asyncio.sleep(1)
@self.app.websocket("/ws/chat/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
"""Endpoint WebSocket pour le chat"""
await websocket.accept()
self.active_connections[session_id] = websocket
self.session_data[session_id] = {"history": [], "user_id": None}
logger.info(f"Connexion WebSocket établie: {session_id}")
try:
while True:
data = await websocket.receive_json()
message_type = data.get("type", "message")
content = data.get("content", "")
if message_type == "message":
# Créer le message avec priorité
priority = MessagePriority.HIGH if data.get("stream", False) else MessagePriority.NORMAL
queued_message = QueuedMessage(
id=str(uuid.uuid4()),
session_id=session_id,
user_id=data.get("user_id", "anonymous"),
content=content,
priority=priority,
metadata={"client_timestamp": data.get("timestamp")}
)
# Ajouter à la queue
success = await self.queue_manager.enqueue(queued_message)
if success:
await websocket.send_json({
"type": "queued",
"message_id": queued_message.id,
"queue_position": "pending",
"timestamp": time.time()
})
else:
await websocket.send_json({
"type": "error",
"error": "Failed to queue message",
"timestamp": time.time()
})
elif message_type == "ping":
await websocket.send_json({"type": "pong", "timestamp": time.time()})
elif message_type == "history":
history = self.session_data.get(session_id, {}).get("history", [])
await websocket.send_json({
"type": "history",
"messages": history[-20:],
"timestamp": time.time()
})
except WebSocketDisconnect:
logger.info(f"Déconnexion WebSocket: {session_id}")
except Exception as e:
logger.error(f"Erreur WebSocket {session_id}: {e}")
finally:
if session_id in self.active_connections:
del self.active_connections[session_id]
if session_id in self.session_data:
del self.session_data[session_id]
Démarrage
app_instance = WebSocketAIServer()
@app_instance.app.on_event("startup")
async def startup():
await app_instance.setup()
@app_instance.app.get("/health")
async def health():
return JSONResponse({"status": "healthy", "queue_size": "OK"})
if __name__ == "__main__":
import uvicorn
uvicorn.run(app_instance.app, host="0.0.0.0", port=8000)
Client WebSocket avec Batch Processing Intelligent
Maintenant, créons un client complet qui gère intelligemment les envois et la réception.
"""
Client WebSocket pour AI Chat avec Batch Processing et Retry Logic
Compatible HolySheep AI - https://www.holysheep.ai
"""
import asyncio
import json
import time
import uuid
import aiohttp
from typing import Optional, Callable, Awaitable
from dataclasses import dataclass, field
from enum import Enum
import logging
from collections import deque
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ConnectionState(Enum):
DISCONNECTED = "disconnected"
CONNECTING = "connecting"
CONNECTED = "connected"
RECONNECTING = "reconnecting"
ERROR = "error"
@dataclass
class PendingMessage:
id: str
content: str
timestamp: float
future: asyncio.Future
retry_count: int = 0
max_retries: int = 3
@dataclass
class BatchConfig:
max_batch_size: int = 10
batch_timeout_ms: int = 100
enable_batching: bool = True
class WebSocketAIError(Exception):
"""Exception de base pour les erreurs WebSocket AI"""
pass
class ConnectionError(WebSocketAIError):
"""Erreur de connexion"""
pass
class TimeoutError(WebSocketAIError):
"""Erreur de timeout"""
pass
class RateLimitError(WebSocketAIError):
"""Erreur de limite de taux"""
pass
class WebSocketAIClient:
"""
Client WebSocket pour chat IA avec batch processing intelligent.
Gère automatiquement le reconnect, le retry, et le batch processing.
"""
def __init__(
self,
base_url: str = "ws://localhost:8000/ws/chat",
session_id: Optional[str] = None,
user_id: str = "anonymous",
batch_config: Optional[BatchConfig] = None,
auto_reconnect: bool = True,
max_reconnect_attempts: int = 5
):
self.base_url = base_url
self.session_id = session_id or str(uuid.uuid4())
self.user_id = user_id
self.batch_config = batch_config or BatchConfig()
self.websocket: Optional[aiohttp.ClientWebSocketResponse] = None
self.session: Optional[aiohttp.ClientSession] = None
self.state = ConnectionState.DISCONNECTED
# Message management
self.pending_messages: dict[str, PendingMessage] = {}
self.pending_batch: deque[PendingMessage] = deque()
self.batch_lock = asyncio.Lock()
self.batch_timer_task: Optional[asyncio.Task] = None
# Auto-reconnect
self.auto_reconnect = auto_reconnect
self.max_reconnect_attempts = max_reconnect_attempts
self.reconnect_attempts = 0
# Callbacks
self.on_message: Optional[Callable[[str, str], Awaitable[None]]] = None
self.on_queue_update: Optional[Callable[[str], Awaitable[None]]] = None
self.on_connection_change: Optional[Callable[[ConnectionState], Awaitable[None]]] = None
# Stats
self.stats = {
"messages_sent": 0,
"messages_received": 0,
"errors": 0,
"reconnects": 0,
"avg_latency_ms": 0
}
async def connect(self) -> bool:
"""Établit la connexion WebSocket"""
if self.state == ConnectionState.CONNECTED:
return True
self._set_state(ConnectionState.CONNECTING)
try:
self.session = aiohttp.ClientSession()
ws_url = f"{self.base_url}/{self.session_id}"
self.websocket = await self.session.ws_connect(
ws_url,
timeout=aiohttp.ClientTimeout(total=10),
autoclose=False
)
self.reconnect_attempts = 0
self._set_state(ConnectionState.CONNECTED)
# Démarrer les tâches de fond
asyncio.create_task(self._receive_loop())
asyncio.create_task(self._batch_processor())
asyncio.create_task(self._ping_loop())
logger.info(f"Connecté au serveur WebSocket: {self.session_id}")
return True
except aiohttp.WSServerHandshakeError as e:
self._set_state(ConnectionState.ERROR)
raise ConnectionError(f"401 Unauthorized ou erreur d'authentification: {e}")
except aiohttp.ClientConnectorError as e:
self._set_state(ConnectionState.ERROR)
raise ConnectionError(f"ConnectionError: impossible de se connecter au serveur: {e}")
except Exception as e:
self._set_state(ConnectionState.ERROR)
raise ConnectionError(f"ConnectionError inattendue: {e}")
async def _set_state(self, new_state: ConnectionState):
"""Met à jour l'état de connexion"""
if self.state != new_state:
self.state = new_state
if self.on_connection_change:
await self.on_connection_change(new_state)
async def _receive_loop(self):
"""Boucle de réception des messages"""
try:
while self.state == ConnectionState.CONNECTED:
msg = await self.websocket.receive()
if msg.type == aiohttp.WSMsgType.TEXT:
await self._handle_message(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
logger.error(f"Erreur WebSocket: {msg.data}")
break
elif msg.type == aiohttp.WSMsgType.CLOSE:
logger.warning("Serveur a fermé la connexion")
break
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Erreur dans la boucle de réception: {e}")
self.stats["errors"] += 1
await self._handle_disconnect()
async def _handle_message(self, data: str):
"""Traite un message reçu"""
try:
msg_data = json.loads(data)
msg_type = msg_data.get("type", "unknown")
if msg_type == "response":
message_id = msg_data.get("message_id")
content = msg_data.get("content")
if message_id and message_id in self.pending_messages:
pending = self.pending_messages.pop(message_id)
if not pending.future.done():
pending.future.set_result(content)
if self.on_message:
await self.on_message(message_id, content)
self.stats["messages_received"] += 1
elif msg_type == "queued":
message_id = msg_data.get("message_id")
if self.on_queue_update:
await self.on_queue_update(message_id)
elif msg_type == "error":
error_msg = msg_data.get("error", "Unknown error")
logger.error(f"Erreur服务器: {error_msg}")
# Check for specific errors
if "401" in error_msg:
raise ConnectionError("401 Unauthorized - Vérifiez votre clé API")
elif "timeout" in error_msg.lower():
raise TimeoutError(f"ConnectionError: timeout - {error_msg}")
elif msg_type == "pong":
pass # Heartbeat response
except json.JSONDecodeError:
logger.warning(f"Message JSON invalide: {data[:100]}")
async def _batch_processor(self):
"""Traite les messages en batch pour le valley filling"""
if not self.batch_config.enable_batching:
return
while True:
try:
async with self.batch_lock:
if self.pending_batch:
# Traiter le batch
batch = []
while self.pending_batch and len(batch) < self.batch_config.max_batch_size:
batch.append(self.pending_batch.popleft())
for pending in batch:
await self._send_single_message(pending)
await asyncio.sleep(self.batch_config.batch_timeout_ms / 1000)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Erreur batch processor: {e}")
async def _send_single_message(self, pending: PendingMessage):
"""Envoie un message unique"""
try:
await self.websocket.send_json({
"type": "message",
"content": pending.content,
"user_id": self.user_id,
"timestamp": time.time()
})
self.stats["messages_sent"] += 1
except Exception as e:
if pending.retry_count < pending.max_retries:
pending.retry_count += 1
self.pending_batch.append(pending)
else:
if not pending.future.done():
pending.future.set_exception(
ConnectionError(f"Échec après {pending.max_retries} tentatives: {e}")
)
async def send_message(
self,
content: str,
timeout: float = 30.0,
priority: bool = False
) -> str:
"""
Envoie un message et attend la réponse.
Args:
content: Contenu du message
timeout: Timeout en secondes
priority: Si True, bypass le batch processing (peak shaving)
Returns:
Réponse du serveur IA
"""
if self.state != ConnectionState.CONNECTED:
raise ConnectionError("Non connecté au serveur WebSocket")
message_id = str(uuid.uuid4())
future = asyncio.Future()
pending = PendingMessage(
id=message_id,
content=content,
timestamp=time.time(),
future=future
)
self.pending_messages[message_id] = pending
if priority or not self.batch_config.enable_batching:
# Envoi immédiat (peak shaving)
await self._send_single_message(pending)
else:
# Ajout au batch (valley filling)
async with self.batch_lock:
self.pending_batch.append(pending)
try:
result = await asyncio.wait_for(future, timeout=timeout)
return result
except asyncio.TimeoutError:
self.pending_messages.pop(message_id, None)
raise TimeoutError(f"ConnectionError: timeout après {timeout}s")
async def send_batch(
self,
messages: list[str],
timeout: float = 60.0
) -> list[str]:
"""
Envoie plusieurs messages en une seule opération de batch.
Optimisé pour le valley filling.
"""
if self.state != ConnectionState.CONNECTED:
raise ConnectionError("Non connecté")
tasks = [
self.send_message(content, timeout=timeout, priority=False)
for content in messages
]
return await asyncio.gather(*tasks, return_exceptions=True)
async def _ping_loop(self):
"""Envoie des ping périodiques pour maintenir la connexion"""
while self.state == ConnectionState.CONNECTED:
try:
await asyncio.sleep(30)
if self.state == ConnectionState.CONNECTED:
await self.websocket.send_json({
"type": "ping",
"timestamp": time.time()
})
except asyncio.CancelledError:
break
except Exception as e:
logger.warning(f"Erreur ping: {e}")
async def _handle_disconnect(self):
"""Gère la déconnexion avec reconnect automatique"""
if not self.auto_reconnect:
await self.disconnect()
return
if self.reconnect_attempts < self.max_reconnect_attempts:
self._set_state(ConnectionState.RECONNECTING)
self.reconnect_attempts += 1
self.stats["reconnects"] += 1
delay = min(2 ** self.reconnect_attempts, 30) # Exponential backoff
logger.info(f"Tentative de reconnexion #{self.reconnect_attempts} dans {delay}s")
await asyncio.sleep(delay)
try:
await self.connect()
except Exception as e:
logger.error(f"Échec de reconnexion: {e}")
else:
logger.error("Nombre max de tentatives de reconnexion atteint")
await self.disconnect()
async def disconnect(self):
"""Ferme la connexion proprement"""
self._set_state(ConnectionState.DISCONNECTED)
if self.batch_timer_task:
self.batch_timer_task.cancel()
if self.websocket:
await self.websocket.close()
if self.session:
await self.session.close()
# Résoudre les messages en attente avec erreur
for pending in self.pending_messages.values():
if not pending.future.done():
pending.future.set_exception(
ConnectionError("Connexion fermée")
)
self.pending_messages.clear()
self.pending_batch.clear()
def get_stats(self) -> dict:
"""Retourne les statistiques de connexion"""
return {
**self.stats,
"state": self.state.value,
"pending_messages": len(self.pending_messages),
"pending_batch": len(self.pending_batch),
"reconnect_attempts": self.reconnect_attempts
}
Exemple d'utilisation
async def example_usage():
"""Exemple complet d'utilisation du client WebSocket AI"""
client = WebSocketAIClient(
batch_config=BatchConfig(
max_batch_size=10,
batch_timeout_ms=100,
enable_batching=True
),
auto_reconnect=True
)
async def on_message(message_id: str, content: str):
print(f"Message reçu [{message_id}]: {content[:100]}...")
async def on_connection_change(state: ConnectionState):
print(f"État de connexion: {state.value}")
client.on_message = on_message
client.on_connection_change = on_connection_change
try:
# Connexion
await client.connect()
print(f"Connecté avec session ID: {client.session_id}")
# Message simple
response = await client.send_message(
"Explique-moi la différence entre peak shaving et valley filling",
priority=True # Urgent
)
print(f"Réponse: {response}")
# Batch de messages (valley filling)
batch_responses = await client.send_batch([
"Qu'est-ce que Redis Streams?",
"Comment implémenter une priority queue?",
"Explique le pattern CQRS"
])
for i, resp in enumerate(batch_responses):
if isinstance(resp, Exception):
print(f"Message {i} a échoué: {resp}")
else:
print(f"Message {i}: {resp[:50]}...")
# Statistiques
print(f"Statistiques: {client.get_stats()}")
except ConnectionError as e:
print(f"Erreur de connexion: {e}")
except TimeoutError as e:
print(f"Timeout: {e}")
except Exception as e:
print(f"Erreur inattendue: {e}")
finally:
await client.disconnect()
if __name__ == "__main__":
asyncio.run(example_usage())
Dashboard de Monitoring avec Métriques en Temps Réel
Un système de message queue robuste nécessite un dashboard de monitoring. Voici mon implémentation complète avec métriques de performance.
"""
Dashboard de Monitoring pour WebSocket AI avec Message Queue
Intégration HolySheep AI - https://www.holysheep.ai
"""
import asyncio
import time
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from collections import deque
import redis.asyncio as redis
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.responses import HTMLResponse
import uvicorn
@dataclass
class QueueMetrics:
"""Métriques de la file de messages"""
current_size: int = 0
processing_size: int = 0
dlq_size: int = 0
avg_wait_time_ms: float = 0.0
avg_processing_time_ms: float = 0.0
messages_per_second: float = 0.0
peak_load: int = 0
timestamp: float = field(default_factory=time.time)
@dataclass
class AIProviderMetrics:
"""Métriques par provider IA"""
provider: str
requests_total: int = 0
requests_success: int = 0
requests_failed: int = 0
avg_latency_ms: float = 0.0
avg_cost_per_1k_tokens: float = 0.0
total_tokens: int = 0
total_cost_usd: float = 0.0
errors: List[str] = field(default_factory=list)
class MetricsCollector:
"""Collecteur de métriques temps réel"""
def __init__(self, redis_url: str = "redis://localhost:637