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 :
- Taux d'erreur 429 (rate limit) : 23% des requêtes pendant les pics
- Erreurs 502 (passerelle indisponible) : 8% supplémentaires
- Perte complète du contexte quand un agent tombait en échec
- Coût explosif : $847/jour avec des ratésiti
- Latence moyenne : 4.2 secondes (inacceptable pour le chat client)
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