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 :

Architecture de la Solution Complète

Notre architecture repose sur quatre composants principaux :

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