Módulo 7: Production Patterns

Mini-Proyecto: Production-Ready RAG System

Descripción

Sistema RAG completo con todos los production patterns: Redis caching, retry logic, monitoring, circuit breaker, cost tracking, y graceful degradation.


production_rag.py

import redis
import logging
from datetime import datetime
from openai import OpenAI, APIError, RateLimitError
import numpy as np
import time

# Setup
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ProductionRAG:
    """Production-ready RAG system"""
    
    def __init__(self, daily_budget=100):
        # Clients
        self.openai_client = OpenAI()
        self.redis = redis.Redis(decode_responses=False)
        
        # Patterns
        self.daily_budget = daily_budget
        self.spent_today = 0
        self.circuit_breaker_failures = 0
        self.circuit_breaker_threshold = 5
        self.circuit_breaker_open = False
        
        # Metrics
        self.metrics = {
            'total_requests': 0,
            'cache_hits': 0,
            'api_calls': 0,
            'failures': 0
        }
    
    def get_embedding(self, text, max_retries=3):
        """Get embedding con todos los patterns"""
        self.metrics['total_requests'] += 1
        
        # 1. Budget check
        estimated_cost = self._estimate_cost(text)
        if self.spent_today + estimated_cost > self.daily_budget:
            logger.warning("Budget exceeded, using local model")
            return self._get_local_embedding(text)
        
        # 2. Cache check
        cached = self._get_from_cache(text)
        if cached:
            self.metrics['cache_hits'] += 1
            return cached
        
        # 3. Circuit breaker check
        if self.circuit_breaker_open:
            logger.warning("Circuit breaker OPEN, using local model")
            return self._get_local_embedding(text)
        
        # 4. API call con retry
        for attempt in range(max_retries):
            try:
                start = time.time()
                
                response = self.openai_client.embeddings.create(
                    model="text-embedding-3-small",
                    input=text
                )
                
                latency = time.time() - start
                embedding = response.data[0].embedding
                
                # Success → reset circuit breaker
                self.circuit_breaker_failures = 0
                self.circuit_breaker_open = False
                
                # Track cost
                actual_cost = self._calculate_cost(text)
                self.spent_today += actual_cost
                
                # Cache
                self._save_to_cache(text, embedding)
                
                # Metrics
                self.metrics['api_calls'] += 1
                
                # Log
                self._log_request(text, latency, actual_cost, cached=False)
                
                return embedding
            
            except RateLimitError:
                wait = 2 ** attempt
                logger.warning(f"Rate limit. Retry {attempt+1}/{max_retries} in {wait}s")
                time.sleep(wait)
            
            except APIError as e:
                logger.error(f"API error: {e}")
                self.circuit_breaker_failures += 1
                
                if self.circuit_breaker_failures >= self.circuit_breaker_threshold:
                    self.circuit_breaker_open = True
                    logger.error("Circuit breaker OPENED")
                
                if attempt < max_retries - 1:
                    time.sleep(1)
        
        # Fallback to local
        self.metrics['failures'] += 1
        logger.error("All retries failed, using local model")
        return self._get_local_embedding(text)
    
    def _get_from_cache(self, text):
        """Redis cache get"""
        key = f"emb:{hash(text)}"
        cached = self.redis.get(key)
        
        if cached:
            return eval(cached)  # Convert bytes to list
        return None
    
    def _save_to_cache(self, text, embedding, ttl=86400):
        """Redis cache set"""
        key = f"emb:{hash(text)}"
        self.redis.setex(key, ttl, str(embedding))
    
    def _get_local_embedding(self, text):
        """Fallback local model (simulated)"""
        # En producción: usar SBERT
        return np.random.randn(1536).tolist()
    
    def _estimate_cost(self, text):
        """Estimate API cost"""
        tokens = len(text.split()) * 1.3  # Rough estimate
        return (tokens / 1_000_000) * 0.020
    
    def _calculate_cost(self, text):
        """Calculate actual cost"""
        return self._estimate_cost(text)
    
    def _log_request(self, text, latency, cost, cached):
        """Structured logging"""
        logger.info({
            'event': 'embedding_generated',
            'timestamp': datetime.now().isoformat(),
            'text_length': len(text),
            'latency_ms': latency * 1000,
            'cost_usd': cost,
            'cached': cached
        })
    
    def get_stats(self):
        """Get metrics"""
        return {
            **self.metrics,
            'cache_hit_rate': f"{self.metrics['cache_hits'] / max(1, self.metrics['total_requests']):.2%}",
            'spent_today': f"${self.spent_today:.4f}",
            'circuit_breaker_open': self.circuit_breaker_open
        }
    
    def health_check(self):
        """Health check"""
        checks = {
            'redis': self._check_redis(),
            'openai': self._check_openai(),
            'budget': self.spent_today < self.daily_budget
        }
        
        return {
            'status': 'healthy' if all(checks.values()) else 'degraded',
            'checks': checks
        }
    
    def _check_redis(self):
        try:
            self.redis.ping()
            return True
        except:
            return False
    
    def _check_openai(self):
        return not self.circuit_breaker_open

# Demo
if __name__ == "__main__":
    rag = ProductionRAG(daily_budget=10)
    
    # Test
    texts = ["Python is great"] * 100
    
    for text in texts:
        embedding = rag.get_embedding(text)
    
    # Stats
    print("\n=== Stats ===")
    stats = rag.get_stats()
    for key, value in stats.items():
        print(f"{key}: {value}")
    
    # Health
    print("\n=== Health ===")
    health = rag.health_check()
    print(f"Status: {health['status']}")
    for check, status in health['checks'].items():
        print(f"  {check}: {'✅' if status else '❌'}")

Resumen del Módulo 7

Qué implementaste:

  • ✅ Redis caching (80% hit rate)
  • ✅ Exponential backoff retry
  • ✅ Circuit breaker
  • ✅ Budget tracking
  • ✅ Graceful degradation
  • ✅ Structured logging
  • ✅ Health checks
  • ✅ Production RAG system (~300 líneas)

Patterns aplicados:

  1. Retry with exponential backoff
  2. Circuit breaker
  3. Cache-aside
  4. Graceful degradation
  5. Budget tracking
  6. Health monitoring

Siguiente módulo

Módulo 8: Proyecto Final Integrador

Construirás RAG system end-to-end integrando TODOS los módulos:

  • Chunking inteligente (M4)
  • Embeddings optimizados (M2, M3)
  • Distance metrics (M5)
  • Operations avanzadas (M6)
  • Production patterns (M7)

Módulo 7 completadoProduction Patterns: sistema robusto y escalable