Módulo 2: OpenAI API - Introducción

Mini-Proyecto: Chatbot de Soporte Técnico

Descripción del proyecto

Este es el proyecto integrador del Módulo 2. Aplicarás TODO lo aprendido para construir un chatbot de soporte técnico production-ready.

Características:

  • ✅ Conversaciones con contexto (recuerda historial)
  • ✅ System message personalizado (comportamiento específico)
  • ✅ Parameters optimizados (temperature, max_tokens)
  • ✅ Error handling robusto (retries, logging)
  • ✅ Cost tracking (monitorea gasto)
  • ✅ Persistence (guarda conversaciones en JSON)

Tiempo: 60-90 minutos
Dificultad: Media-Alta


🎯 Objetivo del proyecto

Crear chatbot CLI que:

  1. Responda preguntas de soporte técnico
  2. Mantenga contexto conversacional
  3. Maneje errores gracefully (no crashea)
  4. Trackee costo por conversación
  5. Guarde logs para análisis

📋 Especificaciones

Funcionalidad:

Rol del chatbot:

  • Asistente de soporte técnico para app "TechApp"
  • Responde FAQs sobre password reset, billing, features
  • Escala a humano si no puede resolver

Comandos especiales:

  • salir / exit → Termina conversación
  • historial → Muestra historial completo
  • costo → Muestra costo acumulado

Persistencia:

  • Guarda cada conversación en conversations/conversation_TIMESTAMP.json
  • Logs de errores en logs/errors.log

💻 Implementación

Estructura del proyecto:

chatbot-soporte/
├── .env                    # API key
├── .gitignore             # Previene leak
├── chatbot.py             # Código principal
├── conversations/         # Conversaciones guardadas
│   └── conversation_20240215_103045.json
└── logs/
    └── errors.log         # Logs de errores

Código completo (chatbot.py):

#!/usr/bin/env python3
"""
Chatbot de Soporte Técnico - TechApp
Módulo 2: OpenAI API - Proyecto Final
"""

import os
import json
import logging
from datetime import datetime
from typing import Optional, List, Dict
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI, RateLimitError, APIError, APITimeoutError

# ============================================================================
# CONFIGURACIÓN
# ============================================================================

load_dotenv()

# Crear directorios
Path("conversations").mkdir(exist_ok=True)
Path("logs").mkdir(exist_ok=True)

# Logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('logs/errors.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# Cliente OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# ============================================================================
# CONFIGURACIÓN DEL CHATBOT
# ============================================================================

SYSTEM_MESSAGE = """
Eres un asistente de soporte técnico para TechApp, una aplicación de productividad.

Tu rol:
1. Ayudar a usuarios con preguntas frecuentes
2. Ser amigable, claro y conciso
3. Si no sabes algo, di: "Déjame escalarlo a un agente humano"
4. NUNCA inventes información (precios, fechas, features)

Información sobre TechApp:
- Password reset: Settings > Security > Reset Password
- Billing: $10/mes plan básico, $30/mes plan premium
- Features: Task management, Calendar, Notas, Sync multi-device
- Soporte: support@techapp.com

Responde siempre en español, máximo 4 frases.
""".strip()

# ============================================================================
# CLASE CHATBOT
# ============================================================================

class TechSupportBot:
    """Chatbot de soporte técnico con OpenAI."""
    
    def __init__(self):
        self.messages: List[Dict[str, str]] = [
            {"role": "system", "content": SYSTEM_MESSAGE}
        ]
        self.total_tokens = 0
        self.total_cost = 0.0
        self.conversation_id = datetime.now().strftime("%Y%m%d_%H%M%S")
        
    def chat(self, user_message: str, max_retries: int = 3) -> Optional[str]:
        """
        Envía mensaje y retorna respuesta con retry.
        
        Args:
            user_message: Mensaje del usuario
            max_retries: Intentos máximos si hay error
            
        Returns:
            Respuesta del bot o None si falla
        """
        
        # Añadir mensaje del usuario
        self.messages.append({"role": "user", "content": user_message})
        
        # Retry con exponential backoff
        for attempt in range(max_retries):
            try:
                response = client.chat.completions.create(
                    model="gpt-3.5-turbo",
                    messages=self.messages,
                    temperature=0.3,  # Bajo (soporte consistente)
                    max_tokens=150,   # Respuestas cortas
                    timeout=30.0
                )
                
                # Extraer respuesta
                assistant_message = response.choices[0].message.content
                self.messages.append({"role": "assistant", "content": assistant_message})
                
                # Actualizar métricas
                self._update_metrics(response.usage)
                
                logger.info(f"✅ Request exitoso | Tokens: {response.usage.total_tokens}")
                return assistant_message
                
            except RateLimitError:
                logger.warning(f"⚠️ Rate limit | Attempt {attempt + 1}/{max_retries}")
                if attempt == max_retries - 1:
                    logger.error("❌ Rate limit persistente")
                    return None
                time.sleep(2 ** attempt)
                
            except (APIError, APITimeoutError) as e:
                logger.warning(f"⚠️ API error | Attempt {attempt + 1}/{max_retries}")
                if attempt == max_retries - 1:
                    logger.error(f"❌ Error persistente: {e}")
                    return None
                time.sleep(2 ** attempt)
                
            except Exception as e:
                logger.error(f"❌ Error inesperado: {type(e).__name__} | {e}")
                return None
        
        return None
    
    def _update_metrics(self, usage):
        """Actualiza métricas de costo y tokens."""
        self.total_tokens += usage.total_tokens
        
        # Pricing GPT-3.5-turbo (Feb 2026)
        input_cost = (usage.prompt_tokens / 1_000_000) * 0.50
        output_cost = (usage.completion_tokens / 1_000_000) * 1.50
        self.total_cost += input_cost + output_cost
    
    def show_history(self):
        """Muestra historial de conversación."""
        print("\n" + "="*60)
        print("HISTORIAL DE CONVERSACIÓN")
        print("="*60)
        
        for i, msg in enumerate(self.messages[1:], 1):  # Skip system
            role = "TÚ" if msg["role"] == "user" else "BOT"
            print(f"\n[{i}] {role}: {msg['content']}")
        
        print("\n" + "="*60 + "\n")
    
    def show_cost(self):
        """Muestra costo acumulado."""
        print(f"\n💰 Costo acumulado: ${self.total_cost:.6f}")
        print(f"📊 Tokens totales: {self.total_tokens}")
        print(f"📝 Mensajes: {len(self.messages) - 1}\n")  # -1 por system
    
    def save_conversation(self):
        """Guarda conversación en JSON."""
        filename = f"conversations/conversation_{self.conversation_id}.json"
        
        data = {
            "conversation_id": self.conversation_id,
            "timestamp": datetime.now().isoformat(),
            "messages": self.messages[1:],  # Skip system message
            "total_tokens": self.total_tokens,
            "total_cost": self.total_cost,
            "message_count": len(self.messages) - 1
        }
        
        with open(filename, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        
        logger.info(f"💾 Conversación guardada: {filename}")
        print(f"💾 Conversación guardada: {filename}")

# ============================================================================
# MAIN - CLI INTERFACE
# ============================================================================

def main():
    """CLI principal del chatbot."""
    
    # Banner
    print("\n" + "="*60)
    print("🤖 CHATBOT DE SOPORTE TÉCNICO - TechApp")
    print("="*60)
    print("\nComandos especiales:")
    print("  - 'salir' / 'exit'  → Terminar conversación")
    print("  - 'historial'       → Ver historial completo")
    print("  - 'costo'           → Ver costo acumulado")
    print("\n" + "="*60 + "\n")
    
    # Crear bot
    bot = TechSupportBot()
    
    # Loop principal
    while True:
        try:
            # Input usuario
            user_input = input("Tú: ").strip()
            
            # Comandos especiales
            if user_input.lower() in ["salir", "exit", "quit"]:
                print("\n👋 ¡Gracias por contactar a TechApp! Hasta pronto.\n")
                bot.show_cost()
                bot.save_conversation()
                break
            
            if user_input.lower() == "historial":
                bot.show_history()
                continue
            
            if user_input.lower() == "costo":
                bot.show_cost()
                continue
            
            # Validar input
            if not user_input:
                print("⚠️ Por favor escribe un mensaje.\n")
                continue
            
            # Enviar a bot
            response = bot.chat(user_input)
            
            if response:
                print(f"\nBot: {response}\n")
            else:
                print("\n❌ Lo siento, hubo un error. Por favor intenta de nuevo.\n")
        
        except KeyboardInterrupt:
            print("\n\n👋 Conversación interrumpida por usuario.\n")
            bot.show_cost()
            bot.save_conversation()
            break
        
        except Exception as e:
            logger.critical(f"❌ Error crítico en main loop: {e}")
            print(f"\n❌ Error inesperado: {e}\n")
            bot.save_conversation()
            break

if __name__ == "__main__":
    # Verificar API key
    if not os.getenv("OPENAI_API_KEY"):
        print("❌ ERROR: OPENAI_API_KEY no encontrada en .env")
        exit(1)
    
    main()

🚀 Uso

1. Ejecutar:

python chatbot.py

2. Conversación ejemplo:

============================================================
🤖 CHATBOT DE SOPORTE TÉCNICO - TechApp
============================================================

Comandos especiales:
  - 'salir' / 'exit'  → Terminar conversación
  - 'historial'       → Ver historial completo
  - 'costo'           → Ver costo acumulado

============================================================

Tú: Hola, ¿cómo reseteo mi contraseña?

Bot: Para resetear tu contraseña en TechApp:
1. Ve a Settings
2. Selecciona Security
3. Click en Reset Password
4. Recibirás un email con instrucciones

Tú: No recibo el email

Bot: Si no recibiste el email de reset:
1. Verifica tu carpeta de Spam/Junk
2. Confirma que el email registrado es correcto
3. Espera 5 minutos (a veces hay delay)
Si sigue sin llegar, déjame escalarlo a un agente humano.

Tú: historial

============================================================
HISTORIAL DE CONVERSACIÓN
============================================================

[1] TÚ: Hola, ¿cómo reseteo mi contraseña?

[2] BOT: Para resetear tu contraseña en TechApp:
1. Ve a Settings
2. Selecciona Security
3. Click en Reset Password
4. Recibirás un email con instrucciones

[3] TÚ: No recibo el email

[4] BOT: Si no recibiste el email de reset:
1. Verifica tu carpeta de Spam/Junk
2. Confirma que el email registrado es correcto
3. Espera 5 minutos (a veces hay delay)
Si sigue sin llegar, déjame escalarlo a un agente humano.

============================================================

Tú: costo

💰 Costo acumulado: $0.000456
📊 Tokens totales: 285
📝 Mensajes: 4

Tú: salir

👋 ¡Gracias por contactar a TechApp! Hasta pronto.

💰 Costo acumulado: $0.000456
📊 Tokens totales: 285
📝 Mensajes: 4

💾 Conversación guardada: conversations/conversation_20240215_103045.json

📊 Análisis de Conversaciones

Script para analizar costos:

import json
from pathlib import Path

def analyze_conversations():
    """Analiza todas las conversaciones guardadas."""
    
    conversations_dir = Path("conversations")
    json_files = list(conversations_dir.glob("*.json"))
    
    if not json_files:
        print("No hay conversaciones guardadas aún.")
        return
    
    total_cost = 0
    total_tokens = 0
    total_messages = 0
    
    print(f"\n📊 ANÁLISIS DE {len(json_files)} CONVERSACIONES\n")
    print(f"{'ID':<20} {'Mensajes':<10} {'Tokens':<10} {'Costo':<12}")
    print("="*52)
    
    for file in json_files:
        with open(file) as f:
            data = json.load(f)
        
        print(f"{data['conversation_id']:<20} {data['message_count']:<10} "
              f"{data['total_tokens']:<10} ${data['total_cost']:<11.6f}")
        
        total_cost += data['total_cost']
        total_tokens += data['total_tokens']
        total_messages += data['message_count']
    
    print("="*52)
    print(f"{'TOTAL':<20} {total_messages:<10} {total_tokens:<10} ${total_cost:<11.6f}")
    print(f"\nPromedio por conversación: ${total_cost/len(json_files):.6f}\n")

if __name__ == "__main__":
    analyze_conversations()

Ejecuta:

python analyze_conversations.py

✅ Rúbrica de Auto-Evaluación

Funcionalidad (40 puntos):

  • (10 pts) Bot responde coherentemente
  • (10 pts) Mantiene contexto (referencias a mensajes previos)
  • (10 pts) Comandos especiales funcionan (historial, costo, salir)
  • (10 pts) Conversaciones se guardan en JSON

Error Handling (30 puntos):

  • (10 pts) Maneja rate limit con retry
  • (10 pts) Maneja API errors con retry
  • (10 pts) Logging de errores a archivo

Optimización (20 puntos):

  • (10 pts) Temperature optimizado (0.3 para soporte)
  • (10 pts) Max tokens limitado (150)

Cost Tracking (10 puntos):

  • (10 pts) Costo calculado correctamente

Total: ___/100 puntos

Interpretación:

  • 90-100: ✅ Excelente
  • 70-89: ⚠️ Bueno
  • <70: ❌ Revisar

🎯 Extensiones (Opcional)

1. Memoria persistente entre sesiones:

# Al iniciar, cargar conversación previa
def load_last_conversation(self):
    # Buscar último JSON
    # Cargar messages
    pass

2. Streaming responses:

response = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=self.messages,
    stream=True
)

for chunk in response:
    print(chunk.choices[0].delta.content, end="", flush=True)

3. Web UI con Streamlit:

pip install streamlit
import streamlit as st

st.title("🤖 TechApp Support")
user_input = st.text_input("Tú:")
if user_input:
    response = bot.chat(user_input)
    st.write(f"Bot: {response}")

📊 Resumen

Lo que construiste:

  1. Chatbot production-ready:

    • Contexto conversacional
    • System message personalizado
    • Parameters optimizados
  2. Error handling robusto:

    • Retry con exponential backoff
    • Logging a archivo
    • Graceful degradation
  3. Cost tracking:

    • Cálculo automático
    • Análisis de conversaciones
  4. Persistence:

    • JSON por conversación
    • Análisis posterior

Skills dominadas:

  • ✅ OpenAI SDK completo
  • ✅ Conversaciones con contexto
  • ✅ Error handling en producción
  • ✅ Cost optimization
  • ✅ Logging y debugging

¡Felicitaciones! Completaste el Módulo 2.


🔗 Recursos adicionales

  1. OpenAI Best Practices
  2. Streamlit Docs - Para web UI
  3. Prompt Engineering - Mejorar system messages

➡️ Próximo paso

Siguiente módulo: Módulo 3 - LM Studio (Local GUI)

Aprenderás a ejecutar LLMs localmente en tu máquina:

  • Zero costo operativo
  • 100% privacidad
  • API compatible con OpenAI (código reutilizable!)

Tiempo: 2-3 horas


Tiempo estimado: 60-90 minutos
¡Proyecto completado! 🎉