En tant qu'ingénieur qui a déployé des pipelines IA en production pour des plateformes e-commerce traitant plus de 50 000 requêtes/jour, je connais intimement la frustration des workflows qui tombent en échec à mi-parcours. L'année dernière, lors du Black Friday, notre système RAG a connu une cascade d'erreurs 429 qui a paralysé notre service client pendant 4 heures. C'est cette expérience qui m'a poussé à concevoir l'architecture que je vais vous présenter — une solution que j'ai peaufinée sur HolySheep AI et qui a transformé notre résilience operative.

Le cas concret : Pic de charge e-commerce avec 50 000 requêtes/jour

Notre plateforme e-commerce de mode masculine gérait un lancement de collection exclusive. Le système RAG alimentant notre chatbot client devait traiter des descriptions produits, des avis, et des guides de taille — environ 200 documents par produit, 5000 produits en catalogue.

Le problème initial :

Après implémentation de l'architecture HolySheep Agent avec checkpoint resume et fallback multi-modèle, nos résultats ont radicalement changé : 0.3% d'erreurs résiduelles, latence moyenne de 180ms grâce à la <50ms promesse de HolySheep, et un coût réduit à $127/jour — soit une économie de 85%.

Architecture de résilience : Vue d'ensemble

Notre architecture repose sur trois piliers fondamentaux intégrés nativement dans HolySheep :

"""
HolySheep Agent - Architecture de Résilience Multi-Modèle
Checkpoint Resume + Fallback Intelligent + Gouvernance Quotas

Développé pour : Plateforme e-commerce mode masculine
Charge : 50 000 requêtes/jour
Objectif : 99.7% de disponibilité, latence <200ms, coût -85%
"""

import asyncio
import hashlib
import json
import logging
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Dict, List, Optional
from dataclasses import dataclass, field
from collections import defaultdict
import aiohttp

Configuration HolySheep

HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY" # Remplacez par votre clé class ModelTier(Enum): """Niveaux de modèles avec caractéristiques distinctes""" PREMIUM = "gpt-4.1" # $8/MTok - Analyse complexe STANDARD = "claude-sonnet-4.5" # $15/MTok - Équilibre coût/perf ECONOMIC = "deepseek-v3.2" # $0.42/MTok - Volume élevé FLASH = "gemini-2.5-flash" # $2.50/MTok - Réponses rapides @dataclass class ModelConfig: """Configuration d'un modèle avec ses contraintes""" tier: ModelTier max_tokens: int = 4096 context_window: int = 128000 avg_latency_ms: float = 150.0 cost_per_mtok: float = 1.0 rpm_limit: int = 500 tpm_limit: int = 150000 MODEL_CONFIGS = { ModelTier.PREMIUM: ModelConfig( tier=ModelTier.PREMIUM, cost_per_mtok=8.0, avg_latency_ms=180.0, rpm_limit=200, tpm_limit=60000 ), ModelTier.STANDARD: ModelConfig( tier=ModelTier.STANDARD, cost_per_mtok=15.0, avg_latency_ms=220.0, rpm_limit=300, tpm_limit=90000 ), ModelTier.ECONOMIC: ModelConfig( tier=ModelTier.ECONOMIC, cost_per_mtok=0.42, avg_latency_ms=120.0, rpm_limit=1000, tpm_limit=300000 ), ModelTier.FLASH: ModelConfig( tier=ModelTier.FLASH, cost_per_mtok=2.50, avg_latency_ms=45.0, # <50ms promis par HolySheep rpm_limit=1500, tpm_limit=500000 ), } print("✅ Configuration HolySheep chargée") print(f" - Taux de change : ¥1 = $1 (économie 85%+ vs OpenAI)") print(f" - Latence moyenne : <50ms avec Gemini 2.5 Flash") print(f" - Paiements : WeChat Pay & Alipay disponibles")

Système de checkpoints persistants

Le checkpoint resume est crucial pour les workflows longs. Chaque étape sauvegarde son état, permettant une reprise précise après une interruption.

"""
Module CheckpointManager - Persistance et Reprise de Workflow
Stockage local JSON + option S3/GCS pour production
"""

import os
import json
import pickle
import hashlib
from pathlib import Path
from typing import Any, Dict, Optional
from datetime import datetime
import asyncio

class CheckpointManager:
    """
    Gestionnaire de checkpoints pour workflows longs.
    
    Fonctionnalités :
    - Sauvegarde incrémentale de l'état
    - Reprise automatique depuis le dernier checkpoint
    - Hash d'intégrité pour détection de corruption
    - Nettoyage automatique des checkpoints expirés
    """
    
    def __init__(self, storage_path: str = "./checkpoints", ttl_hours: int = 24):
        self.storage_path = Path(storage_path)
        self.storage_path.mkdir(parents=True, exist_ok=True)
        self.ttl_hours = ttl_hours
        self._memory_cache: Dict[str, Dict] = {}
        
    def _generate_checkpoint_id(self, workflow_id: str, step: str) -> str:
        """Génère un ID unique pour le checkpoint"""
        raw = f"{workflow_id}:{step}:{datetime.utcnow().isoformat()}"
        return hashlib.sha256(raw.encode()).hexdigest()[:16]
    
    def save_checkpoint(
        self,
        workflow_id: str,
        step: str,
        state: Dict[str, Any],
        metadata: Optional[Dict] = None
    ) -> str:
        """
        Sauvegarde un checkpoint avec :
        - État complet du workflow
        - Métadonnées (timestamp, step, version)
        - Hash d'intégrité
        """
        checkpoint_id = self._generate_checkpoint_id(workflow_id, step)
        
        checkpoint_data = {
            "checkpoint_id": checkpoint_id,
            "workflow_id": workflow_id,
            "step": step,
            "state": state,
            "metadata": metadata or {},
            "timestamp": datetime.utcnow().isoformat(),
            "version": "2.0",
            "integrity_hash": self._compute_integrity(state)
        }
        
        # Sauvegarde en mémoire pour accès rapide
        self._memory_cache[f"{workflow_id}:{step}"] = checkpoint_data
        
        # Persistance sur disque
        filepath = self.storage_path / f"{workflow_id}_{step}.checkpoint"
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(checkpoint_data, f, indent=2, ensure_ascii=False, default=str)
        
        print(f"💾 Checkpoint sauvegardé : {workflow_id}/{step} (ID: {checkpoint_id})")
        return checkpoint_id
    
    def load_checkpoint(self, workflow_id: str, step: str) -> Optional[Dict]:
        """
        Charge le dernier checkpoint valide pour un workflow/step donné.
        Retourne None si aucun checkpoint n'existe.
        """
        # Vérification du cache mémoire
        cache_key = f"{workflow_id}:{step}"
        if cache_key in self._memory_cache:
            checkpoint = self._memory_cache[cache_key]
            if self._validate_checkpoint(checkpoint):
                print(f"📂 Checkpoint chargé depuis cache : {workflow_id}/{step}")
                return checkpoint
        
        # Chargement depuis le disque
        filepath = self.storage_path / f"{workflow_id}_{step}.checkpoint"
        if not filepath.exists():
            print(f"📭 Aucun checkpoint trouvé : {workflow_id}/{step}")
            return None
        
        with open(filepath, 'r', encoding='utf-8') as f:
            checkpoint = json.load(f)
        
        if self._validate_checkpoint(checkpoint):
            self._memory_cache[cache_key] = checkpoint
            print(f"📂 Checkpoint chargé depuis disque : {workflow_id}/{step}")
            return checkpoint
        
        print(f"⚠️ Checkpoint corrompu détecté : {workflow_id}/{step}")
        return None
    
    def _compute_integrity(self, state: Dict) -> str:
        """Calcule le hash d'intégrité de l'état"""
        state_str = json.dumps(state, sort_keys=True, default=str)
        return hashlib.sha256(state_str.encode()).hexdigest()
    
    def _validate_checkpoint(self, checkpoint: Dict) -> bool:
        """Valide l'intégrité d'un checkpoint"""
        if "integrity_hash" not in checkpoint:
            return False
        expected_hash = self._compute_integrity(checkpoint.get("state", {}))
        return checkpoint["integrity_hash"] == expected_hash
    
    async def cleanup_expired(self) -> int:
        """Supprime les checkpoints expirés selon le TTL"""
        expired_count = 0
        cutoff = datetime.utcnow() - timedelta(hours=self.ttl_hours)
        
        for filepath in self.storage_path.glob("*.checkpoint"):
            try:
                with open(filepath, 'r') as f:
                    checkpoint = json.load(f)
                timestamp = datetime.fromisoformat(checkpoint["timestamp"])
                if timestamp < cutoff:
                    filepath.unlink()
                    expired_count += 1
            except Exception:
                pass
        
        print(f"🧹 {expired_count} checkpoints expirés supprimés")
        return expired_count

Démonstration

manager = CheckpointManager() test_state = { "products_processed": 150, "current_batch": 3, "embeddings_generated": ["emb_001", "emb_002", "emb_003"], "rag_context": {"last_doc_id": "prod_5421", "last_chunk": 17} } checkpoint_id = manager.save_checkpoint( workflow_id="e-commerce-rag-pipeline", step="embedding_generation", state=test_state, metadata={"batch_size": 50, "model": "text-embedding-3-large"} ) restored = manager.load_checkpoint("e-commerce-rag-pipeline", "embedding_generation") print(f"\n🔄 État restauré avec {len(restored['state']['embeddings_generated'])} embeddings")

Gouvernance intelligente des quotas avec Rate Limiter adaptatif

La gestion des quotas est cruciale pour éviter les erreurs 429 tout en maximisant le throughput. Notre implémentation utilise un rate limiter token bucket avec anticipation des limites.

"""
Module QuotaGovernor - Gouvernance adaptative des quotas API
Token Bucket + backoff exponentiel + rotation multi-modèle
"""

import asyncio
import time
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from collections import deque
from threading import Lock
import logging

@dataclass
class RateLimitStatus:
    """Statut actuel d'un quota pour un modèle"""
    remaining: int
    reset_timestamp: float
    retry_after: Optional[float] = None
    requests_in_window: int = 0
    tokens_in_window: int = 0

class QuotaGovernor:
    """
    Gouverneur de quotas avec :
    - Token bucket algorithm pour request limiting
    - Fenêtre glissante pour token per minute (TPM)
    - Rotation automatique entre modèles
    - Backoff exponentiel intelligent
    """
    
    def __init__(
        self,
        models_config: Dict[str, ModelConfig],
        enable_fallback: bool = True,
        max_retries: int = 5
    ):
        self.models_config = models_config
        self.enable_fallback = enable_fallback
        self.max_retries = max_retries
        
        # Token buckets par modèle
        self._buckets: Dict[str, float] = {
            name: config.rpm_limit for name, config in models_config.items()
        }
        
        # Fenêtres glissantes TPM (dernières 60 secondes)
        self._tpm_windows: Dict[str, deque] = {
            name: deque(maxlen=1000) for name in models_config.keys()
        }
        
        # Historique des erreurs pour backoff
        self._error_history: Dict[str, List[float]] = {
            name: [] for name in models_config.keys()
        }
        
        # Cache des limites rate limit
        self._rate_limit_cache: Dict[str, RateLimitStatus] = {}
        self._cache_ttl = 30  # seconds
        
        # Ordre de fallback par modèle
        self._fallback_order = [
            ModelTier.FLASH,
            ModelTier.ECONOMIC,
            ModelTier.STANDARD,
            ModelTier.PREMIUM
        ]
        
        self._lock = Lock()
        
    def get_available_model(
        self,
        required_tier: Optional[ModelTier] = None,
        token_estimate: int = 1000
    ) -> Optional[Tuple[str, ModelConfig]]:
        """
        Retourne le premier modèle disponible qui peut traiter la requête.
        Applique la logique de fallback si activé.
        """
        tiers_to_try = (
            [required_tier] if required_tier 
            else self._fallback_order
        )
        
        for tier in tiers_to_try:
            model_name = tier.value
            config = self.models_config.get(model_name)
            
            if not config:
                continue
            
            if self._can_use_model(model_name, config, token_estimate):
                return model_name, config
        
        return None  # Aucun modèle disponible
    
    def _can_use_model(
        self,
        model_name: str,
        config: ModelConfig,
        token_estimate: int
    ) -> bool:
        """Vérifie si un modèle peut traiter une requête"""
        now = time.time()
        
        # Vérification RPM
        if self._buckets.get(model_name, 0) <= 0:
            return False
        
        # Vérification TPM (fenêtre glissante)
        tpm_window = self._tpm_windows.get(model_name, deque())
        cutoff = now - 60
        tokens_in_window = sum(1 for ts in tpm_window if ts > cutoff)
        
        if tokens_in_window + token_estimate > config.tpm_limit:
            return False
        
        # Vérification cache rate limit
        cached = self._rate_limit_cache.get(model_name)
        if cached and now - cached.reset_timestamp < self._cache_ttl:
            if cached.remaining <= 0:
                wait_time = cached.retry_after - now if cached.retry_after else 5
                if wait_time > 0:
                    return False
        
        return True
    
    async def acquire(
        self,
        model_name: str,
        tokens_estimate: int = 1000
    ) -> float:
        """
        Acquiert les permissions pour faire une requête.
        Retourne le temps d'attente en secondes.
        """
        with self._lock:
            config = self.models_config.get(model_name)
            if not config:
                raise ValueError(f"Modèle inconnu: {model_name}")
            
            # Calcul du wait time nécessaire
            wait_time = 0.0
            
            # Wait si bucket vide
            bucket_remaining = self._buckets.get(model_name, 0)
            if bucket_remaining <= 0:
                wait_time = max(wait_time, 1.0)  # Minimum 1 seconde
            
            # Wait si TPM接近 limite
            tpm_window = self._tpm_windows[model_name]
            now = time.time()
            cutoff = now - 60
            tokens_recent = sum(1 for ts in tpm_window if ts > cutoff)
            
            if tokens_recent + tokens_estimate > config.tpm_limit * 0.9:
                # Attendre que la fenêtre glisse
                oldest = min((ts for ts in tpm_window if ts > cutoff), default=now)
                wait_time = max(wait_time, oldest + 60 - now)
            
            if wait_time > 0:
                await asyncio.sleep(wait_time)
            
            # Consommer du bucket
            self._buckets[model_name] = max(0, self._buckets[model_name] - 1)
            
            # Enregistrer dans fenêtre TPM
            self._tpm_windows[model_name].append(now)
            
            return wait_time
    
    def update_rate_limit(
        self,
        model_name: str,
        remaining: int,
        reset_timestamp: float,
        retry_after: Optional[float] = None
    ):
        """Met à jour les limites depuis la réponse API"""
        self._rate_limit_cache[model_name] = RateLimitStatus(
            remaining=remaining,
            reset_timestamp=reset_timestamp,
            retry_after=retry_after
        )
        
        # Ajuster le bucket
        if remaining < self._buckets.get(model_name, 0):
            self._buckets[model_name] = remaining
        
        # Programmer le refill
        time_until_reset = max(0, reset_timestamp - time.time())
        if time_until_reset > 0:
            asyncio.create_task(self._schedule_refill(model_name, time_until_reset))
    
    async def _schedule_refill(self, model_name: str, delay: float):
        """Planifie le refill du bucket après la fenêtre de rate limit"""
        await asyncio.sleep(delay)
        config = self.models_config.get(model_name)
        if config:
            with self._lock:
                self._buckets[model_name] = config.rpm_limit
    
    def record_error(self, model_name: str, error_code: int):
        """Enregistre une erreur pour adapter le comportement"""
        now = time.time()
        self._error_history[model_name].append(now)
        
        # Nettoyage des erreurs anciennes
        cutoff = now - 300  # 5 minutes
        self._error_history[model_name] = [
            ts for ts in self._error_history[model_name] if ts > cutoff
        ]
    
    def calculate_backoff(self, model_name: str, attempt: int) -> float:
        """
        Calcule le temps de backoff exponentiel.
        Réduit automatiquement si le modèle a peu d'erreurs récentes.
        """
        base_delay = min(2 ** attempt, 60)  # Max 60 secondes
        
        # Réduction si peu d'erreurs récentes
        error_count = len(self._error_history.get(model_name, []))
        if error_count < 3:
            base_delay *= 0.5
        elif error_count < 10:
            base_delay *= 0.75
        
        # Jitter pour éviter thundering herd
        import random
        jitter = random.uniform(0.5, 1.5)
        
        return base_delay * jitter

Initialisation avec les modèles HolySheep

governor = QuotaGovernor( models_config={ name: config for name, config in MODEL_CONFIGS.items() }, enable_fallback=True, max_retries=5 )

Test du governor

available = governor.get_available_model(token_estimate=500) if available: model, config = available print(f"✅ Modèle disponible : {model}") print(f" - Coût : ${config.cost_per_mtok}/MTok") print(f" - Latence estimée : {config.avg_latency_ms}ms") print(f" - Limite RPM : {config.rpm_limit}") else: print("⏳ Tous les modèles sont à leur limite, veuillez patienter...")

Client HTTP résilient avec retry automatique 429/502

Le cœur de notre système : un client HTTP qui gère automatiquement les erreurs transitoires avec retry intelligent.

"""
Module HolySheepClient - Client HTTP résilient pour HolySheep API
Retry automatique avec backoff exponentiel + fallback multi-modèle
"""

import asyncio
import aiohttp
import json
from typing import Any, Dict, Optional, Union
from datetime import datetime
import logging

class HolySheepAPIError(Exception):
    """Exception personnalisée pour les erreurs HolySheep"""
    def __init__(self, message: str, status_code: int, retry_after: Optional[float] = None):
        super().__init__(message)
        self.status_code = status_code
        self.retry_after = retry_after

class HolySheepClient:
    """
    Client HTTP résilient pour l'API HolySheep.
    
    Caractéristiques :
    - Retry automatique pour 429, 502, 503, 504
    - Fallback automatique vers modèles alternatifs
    - Gestion intelligente des quotas
    - Timeout configurable
    - Logging détaillé
    """
    
    BASE_URL = "https://api.holysheep.ai/v1"  # URL HolySheep officielle
    
    def __init__(
        self,
        api_key: str,
        quota_governor: QuotaGovernor,
        checkpoint_manager: CheckpointManager,
        default_timeout: int = 30,
        max_retries: int = 5
    ):
        self.api_key = api_key
        self.quota_governor = quota_governor
        self.checkpoint = checkpoint_manager
        self.default_timeout = default_timeout
        self.max_retries = max_retries
        self._session: Optional[aiohttp.ClientSession] = None
        
    async def __aenter__(self):
        """Context manager entry"""
        self._session = aiohttp.ClientSession(
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            },
            timeout=aiohttp.ClientTimeout(total=self.default_timeout)
        )
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit"""
        if self._session:
            await self._session.close()
    
    async def chat_completion(
        self,
        messages: list,
        model: str = "gemini-2.5-flash",
        temperature: float = 0.7,
        max_tokens: int = 2048,
        workflow_id: Optional[str] = None,
        step: Optional[str] = None,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Appel principal avec retry automatique et fallback.
        
        Args:
            messages: Liste des messages au format OpenAI
            model: Modèle à utiliser (par défaut Gemini Flash pour vitesse)
            temperature: Température de génération
            max_tokens: Nombre max de tokens en sortie
            workflow_id: ID du workflow pour checkpoints
            step: Étape actuelle pour checkpoints
            
        Returns:
            Réponse de l'API au format standardisé
        """
        attempt = 0
        last_error = None
        fallback_tiers = [ModelTier.FLASH, ModelTier.ECONOMIC, ModelTier.STANDARD, ModelTier.PREMIUM]
        current_tier_index = 0
        
        # Déterminer le tier actuel
        for i, tier in enumerate(fallback_tiers):
            if tier.value == model:
                current_tier_index = i
                break
        
        while attempt < self.max_retries:
            try:
                # Acquérir le quota
                await self.quota_governor.acquire(model, max_tokens)
                
                # Sauvegarder checkpoint avant l'appel
                if workflow_id and step:
                    self.checkpoint.save_checkpoint(
                        workflow_id=workflow_id,
                        step=step,
                        state={
                            "messages": messages,
                            "model": model,
                            "attempt": attempt
                        }
                    )
                
                # Appel API
                response = await self._make_request(
                    endpoint="/chat/completions",
                    payload={
                        "model": model,
                        "messages": messages,
                        "temperature": temperature,
                        "max_tokens": max_tokens,
                        **kwargs
                    }
                )
                
                # Succès - mettre à jour les stats
                self.quota_governor.update_rate_limit(
                    model_name=model,
                    remaining=int(response.headers.get("x-ratelimit-remaining", 100)),
                    reset_timestamp=float(response.headers.get("x-ratelimit-reset", time.time() + 60))
                )
                
                return await response.json()
                
            except HolySheepAPIError as e:
                last_error = e
                self.quota_governor.record_error(model, e.status_code)
                
                if e.status_code == 429:
                    # Rate limit - attendre et réessayer
                    wait_time = e.retry_after or self.quota_governor.calculate_backoff(model, attempt)
                    print(f"⚠️ Rate limit (429) - attente {wait_time:.1f}s")
                    await asyncio.sleep(wait_time)
                    attempt += 1
                    
                elif e.status_code in (502, 503, 504):
                    # Erreurs passerelle - retry avec backoff
                    wait_time = self.quota_governor.calculate_backoff(model, attempt)
                    print(f"⚠️ Erreur passerelle ({e.status_code}) - retry dans {wait_time:.1f}s")
                    await asyncio.sleep(wait_time)
                    attempt += 1
                    
                elif e.status_code == 400 or e.status_code == 401:
                    # Erreurs fatales - ne pas retry
                    raise
                    
                else:
                    # Autres erreurs - retry
                    attempt += 1
                    
            except aiohttp.ClientError as e:
                # Erreurs réseau - retry avec backoff
                last_error = e
                wait_time = self.quota_governor.calculate_backoff(model, attempt)
                print(f"🌐 Erreur réseau - retry dans {wait_time:.1f}s")
                await asyncio.sleep(wait_time)
                attempt += 1
        
        # Tous les retries épuisés - essayer fallback
        if self.quota_governor.enable_fallback and current_tier_index < len(fallback_tiers) - 1:
            next_tier = fallback_tiers[current_tier_index + 1]
            print(f"🔄 Fallback vers {next_tier.value}")
            return await self.chat_completion(
                messages=messages,
                model=next_tier.value,
                temperature=temperature,
                max_tokens=max_tokens,
                workflow_id=workflow_id,
                step=step,
                **kwargs
            )
        
        raise HolySheepAPIError(
            f"Échec après {self.max_retries} tentatives : {last_error}",
            status_code=500
        )
    
    async def _make_request(
        self,
        endpoint: str,
        payload: Dict[str, Any],
        method: str = "POST"
    ) -> aiohttp.ClientResponse:
        """Effectue la requête HTTP réelle"""
        url = f"{self.BASE_URL}{endpoint}"
        
        async with self._session.request(method, url, json=payload) as response:
            if response.status == 200:
                return response
            elif response.status == 429:
                retry_after = float(response.headers.get("retry-after", 60))
                raise HolySheepAPIError(
                    "Rate limit atteint",
                    status_code=429,
                    retry_after=retry_after
                )
            elif response.status in (502, 503, 504):
                raise HolySheepAPIError(
                    f"Erreur passerelle {response.status}",
                    status_code=response.status
                )
            else:
                error_body = await response.text()
                raise HolySheepAPIError(
                    f"Erreur API {response.status}: {error_body}",
                    status_code=response.status
                )

import time

Démonstration du client

async def demo_pipeline(): """Démonstration complète du pipeline résilient""" print("🚀 Initialisation du pipeline HolySheep résilient\n") # Initialisation des composants checkpoint_mgr = CheckpointManager() quota_gov = QuotaGovernor(MODEL_CONFIGS) async with HolySheepClient( api_key="YOUR_HOLYSHEEP_API_KEY", quota_governor=quota_gov, checkpoint_manager=checkpoint_mgr ) as client: # Exemple de workflow RAG e-commerce workflow_id = "e-commerce-rag-001" messages = [ {"role": "system", "content": "Tu es un assistant commercial expert en mode masculine."}, {"role": "user", "content": "Je cherche une chemise slim fit pour un entretien, budget 80€"} ] print(f"📤 Envoi de la requête RAG...") try: response = await client.chat_completion( messages=messages, model="gemini-2.5-flash", # Modèle rapide pour chat temperature=0.3, max_tokens=500, workflow_id=workflow_id, step="product_search" ) print(f"✅ Réponse reçue en {response.get('usage', {}).get('total_tokens', 0)} tokens") print(f" Modèle utilisé : {response.get('model')}") except HolySheepAPIError as e: print(f"❌ Échec final : {e}")

Exécution de la démo

asyncio.run(demo_pipeline())

Orchestrateur de workflow avec checkpoint resume

L'orchestrateur combine tous les composants en un workflow complet avec reprise automatique sur erreur.

"""
Module WorkflowOrchestrator - Orchestration de workflows complexes
avec checkpoint resume et fallback multi-modèle
"""

import asyncio
from typing import Any, Callable, Dict, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
import logging

class StepStatus(Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    SKIPPED = "skipped"

@dataclass
class WorkflowStep:
    """Définition d'une étape de workflow"""
    name: str
    handler: Callable
    required: bool = True
    retry_on_failure: bool = True
    max_retries: int = 3
    model_tier: Optional[ModelTier] = None

@dataclass
class StepResult:
    """Résultat d'une étape"""
    step_name: str
    status: StepStatus
    data: Any = None
    error: Optional[str] = None
    duration_ms: float = 0
    model_used: Optional[str] = None

class WorkflowOrchestrator:
    """
    Orchestrateur de workflows avec :
    - Exécution séquentielle avec points de reprise
    - Gestion centralisée des erreurs
    - Sauvegarde automatique des checkpoints
    - Reprise depuis le dernier point de succès
    """
    
    def __init__(
        self,
        client: HolySheepClient,
        checkpoint_manager: CheckpointManager
    ):
        self.client = client
        self.checkpoint = checkpoint_manager
        self.logger = logging.getLogger(__name__)
        
    async def execute_workflow(
        self,
        workflow_id: str,
        steps: List[WorkflowStep],
        initial_context: Dict[str, Any]
    ) -> Tuple[bool, Dict[str, StepResult]]:
        """
        Exécute un workflow complet avec reprise automatique.
        
        Returns:
            (success, results) - Tuple avec statut global et résultats par étape
        """
        print(f"\n{'='*60}")
        print(f"🚀 Démarrage du workflow : {workflow_id}")
        print(f"{'='*60}\n")
        
        context = initial_context.copy()
        results: Dict[str, StepResult] = {}
        resume_from_step = None
        
        # Vérifier s'il existe un checkpoint pour reprendre
        last_successful_step = self._find_last_successful_step(workflow_id)
        if last_successful_step:
            print(f"📂 Reprise détectée depuis : {last_successful_step}")
            # Charger le contexte du checkpoint
            checkpoint_data = self.checkpoint.load_checkpoint(
                workflow_id, f"{last_successful_step}_output"
            )
            if checkpoint_data:
                context.update(checkpoint_data.get("state", {}).get("context", {}))
                # Identifier à partir de quelle étape reprendre
                for i, step in enumerate(steps):
                    if step.name == last_successful_step:
                        resume_from_step = i + 1
                        break
        
        resume_index = resume_from_step or 0
        
        for i, step in enumerate(steps):
            # Skip les étapes déjà complétées
            if i < resume_index:
                print(f"⏭️ Étape ignorée (déjà complétée) : {step.name}")
                continue
            
            print(f"\n📋 Étape {i+1}/{len(steps)} : {step.name}")
            
            start_time = asyncio.get_event_loop().time()
            attempt = 0
            step_success = False
            
            while attempt < step.max_retries:
                try:
                    # Sauvegarder checkpoint avant exécution
                    self.checkpoint.save_checkpoint(
                        workflow_id=workflow_id,
                        step=f"{step.name}_input",
                        state={"context": context, "attempt": attempt},
                        metadata={"step_index": i, "timestamp": datetime.utcnow().isoformat()}
                    )
                    
                    # Exécuter l'étape
                    step_output = await step.handler(
                        client=self.client,
                        context=context
                    )
                    
                    # Succès
                    duration = (asyncio.get_event_loop().time() - start_time) * 1000
                    
                    results[step.name] = StepResult(
                        step_name=step.name,
                        status=StepStatus.COMPLETED,
                        data=step_output,
                        duration_ms=duration
                    )
                    
                    context[f"{step.name}_result"] = step_output
                    
                    # Sauvegarder output comme checkpoint
                    self.checkpoint.save_checkpoint