Module 2: OpenAI API - Introduction

Mini-Project: Technical Support Chatbot

Project overview

This is the integrative project for Module 2. You'll apply EVERYTHING you've learned to build a production-ready technical support chatbot.

Features:

  • ✅ Conversations with context (remembers history)
  • ✅ Custom system message (specific behavior)
  • ✅ Optimized parameters (temperature, max_tokens)
  • ✅ Robust error handling (retries, logging)
  • ✅ Cost tracking (monitors spending)
  • ✅ Persistence (saves conversations to JSON)

Time: 60-90 minutes
Difficulty: Medium-High


🎯 Project goal

Build a CLI chatbot that:

  1. Answers technical support questions
  2. Maintains conversational context
  3. Handles errors gracefully (doesn't crash)
  4. Tracks cost per conversation
  5. Saves logs for analysis

📋 Specifications

Functionality:

Chatbot role:

  • Technical support assistant for the "TechApp" app
  • Answers FAQs about password reset, billing, features
  • Escalates to a human if it can't resolve the issue

Special commands:

  • salir / exit → Ends the conversation
  • historial → Shows the full history
  • costo → Shows the accumulated cost

Persistence:

  • Saves each conversation to conversations/conversation_TIMESTAMP.json
  • Error logs in logs/errors.log

💻 Implementation

Project structure:

chatbot-soporte/
├── .env                    # API key
├── .gitignore             # Prevents leaks
├── chatbot.py             # Main code
├── conversations/         # Saved conversations
│   └── conversation_20240215_103045.json
└── logs/
    └── errors.log         # Error logs

Full code (chatbot.py):

#!/usr/bin/env python3
"""
Technical Support Chatbot - TechApp
Module 2: OpenAI API - Final Project
"""

import os
import json
import time
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

# ============================================================================
# CONFIGURATION
# ============================================================================

load_dotenv()

# Create directories
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__)

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

# ============================================================================
# CHATBOT CONFIGURATION
# ============================================================================

SYSTEM_MESSAGE = """
You are a technical support assistant for TechApp, a productivity application.

Your role:
1. Help users with frequently asked questions
2. Be friendly, clear, and concise
3. If you don't know something, say: "Let me escalate this to a human agent"
4. NEVER make up information (prices, dates, features)

Information about TechApp:
- Password reset: Settings > Security > Reset Password
- Billing: $10/month basic plan, $30/month premium plan
- Features: Task management, Calendar, Notes, Multi-device sync
- Support: support@techapp.com

Always respond in English, 4 sentences maximum.
""".strip()

# ============================================================================
# CHATBOT CLASS
# ============================================================================

class TechSupportBot:
    """Technical support chatbot with 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]:
        """
        Send a message and return the response with retry.
        
        Args:
            user_message: The user's message
            max_retries: Maximum attempts if there's an error
            
        Returns:
            The bot's response, or None if it fails
        """
        
        # Add the user's message
        self.messages.append({"role": "user", "content": user_message})
        
        # Retry with 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,  # Low (consistent support)
                    max_tokens=150,   # Short answers
                    timeout=30.0
                )
                
                # Extract the response
                assistant_message = response.choices[0].message.content
                self.messages.append({"role": "assistant", "content": assistant_message})
                
                # Update metrics
                self._update_metrics(response.usage)
                
                logger.info(f"✅ Successful request | 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("❌ Persistent rate limit")
                    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"❌ Persistent error: {e}")
                    return None
                time.sleep(2 ** attempt)
                
            except Exception as e:
                logger.error(f"❌ Unexpected error: {type(e).__name__} | {e}")
                return None
        
        return None
    
    def _update_metrics(self, usage):
        """Update cost and token metrics."""
        self.total_tokens += usage.total_tokens
        
        # GPT-3.5-turbo pricing (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):
        """Show the conversation history."""
        print("\n" + "="*60)
        print("CONVERSATION HISTORY")
        print("="*60)
        
        for i, msg in enumerate(self.messages[1:], 1):  # Skip system
            role = "YOU" if msg["role"] == "user" else "BOT"
            print(f"\n[{i}] {role}: {msg['content']}")
        
        print("\n" + "="*60 + "\n")
    
    def show_cost(self):
        """Show the accumulated cost."""
        print(f"\n💰 Accumulated cost: ${self.total_cost:.6f}")
        print(f"📊 Total tokens: {self.total_tokens}")
        print(f"📝 Messages: {len(self.messages) - 1}\n")  # -1 for system
    
    def save_conversation(self):
        """Save the conversation to 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"💾 Conversation saved: {filename}")
        print(f"💾 Conversation saved: {filename}")

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

def main():
    """The chatbot's main CLI."""
    
    # Banner
    print("\n" + "="*60)
    print("🤖 TECHNICAL SUPPORT CHATBOT - TechApp")
    print("="*60)
    print("\nSpecial commands:")
    print("  - 'salir' / 'exit'  → End the conversation")
    print("  - 'historial'       → See the full history")
    print("  - 'costo'           → See the accumulated cost")
    print("\n" + "="*60 + "\n")
    
    # Create the bot
    bot = TechSupportBot()
    
    # Main loop
    while True:
        try:
            # User input
            user_input = input("You: ").strip()
            
            # Special commands
            if user_input.lower() in ["salir", "exit", "quit"]:
                print("\n👋 Thanks for contacting TechApp! See you soon.\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
            
            # Validate input
            if not user_input:
                print("⚠️ Please type a message.\n")
                continue
            
            # Send to the bot
            response = bot.chat(user_input)
            
            if response:
                print(f"\nBot: {response}\n")
            else:
                print("\n❌ Sorry, there was an error. Please try again.\n")
        
        except KeyboardInterrupt:
            print("\n\n👋 Conversation interrupted by the user.\n")
            bot.show_cost()
            bot.save_conversation()
            break
        
        except Exception as e:
            logger.critical(f"❌ Critical error in the main loop: {e}")
            print(f"\n❌ Unexpected error: {e}\n")
            bot.save_conversation()
            break

if __name__ == "__main__":
    # Check the API key
    if not os.getenv("OPENAI_API_KEY"):
        print("❌ ERROR: OPENAI_API_KEY not found in .env")
        exit(1)
    
    main()

🚀 Usage

1. Run it:

python chatbot.py

2. Example conversation:

============================================================
🤖 TECHNICAL SUPPORT CHATBOT - TechApp
============================================================

Special commands:
  - 'salir' / 'exit'  → End the conversation
  - 'historial'       → See the full history
  - 'costo'           → See the accumulated cost

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

You: Hi, how do I reset my password?

Bot: To reset your password in TechApp:
1. Go to Settings
2. Select Security
3. Click Reset Password
4. You'll receive an email with instructions

You: I'm not receiving the email

Bot: If you didn't receive the reset email:
1. Check your Spam/Junk folder
2. Confirm the registered email is correct
3. Wait 5 minutes (there's sometimes a delay)
If it still doesn't arrive, let me escalate this to a human agent.

You: historial

============================================================
CONVERSATION HISTORY
============================================================

[1] YOU: Hi, how do I reset my password?

[2] BOT: To reset your password in TechApp:
1. Go to Settings
2. Select Security
3. Click Reset Password
4. You'll receive an email with instructions

[3] YOU: I'm not receiving the email

[4] BOT: If you didn't receive the reset email:
1. Check your Spam/Junk folder
2. Confirm the registered email is correct
3. Wait 5 minutes (there's sometimes a delay)
If it still doesn't arrive, let me escalate this to a human agent.

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

You: costo

💰 Accumulated cost: $0.000456
📊 Total tokens: 285
📝 Messages: 4

You: salir

👋 Thanks for contacting TechApp! See you soon.

💰 Accumulated cost: $0.000456
📊 Total tokens: 285
📝 Messages: 4

💾 Conversation saved: conversations/conversation_20240215_103045.json

📊 Conversation Analysis

Script to analyze costs:

import json
from pathlib import Path

def analyze_conversations():
    """Analyze all saved conversations."""
    
    conversations_dir = Path("conversations")
    json_files = list(conversations_dir.glob("*.json"))
    
    if not json_files:
        print("No conversations saved yet.")
        return
    
    total_cost = 0
    total_tokens = 0
    total_messages = 0
    
    print(f"\n📊 ANALYSIS OF {len(json_files)} CONVERSATIONS\n")
    print(f"{'ID':<20} {'Messages':<10} {'Tokens':<10} {'Cost':<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"\nAverage per conversation: ${total_cost/len(json_files):.6f}\n")

if __name__ == "__main__":
    analyze_conversations()

Run it:

python analyze_conversations.py

✅ Self-Assessment Rubric

Functionality (40 points):

  • (10 pts) The bot answers coherently
  • (10 pts) Maintains context (references to previous messages)
  • (10 pts) Special commands work (historial, costo, salir)
  • (10 pts) Conversations are saved to JSON

Error Handling (30 points):

  • (10 pts) Handles rate limit with retry
  • (10 pts) Handles API errors with retry
  • (10 pts) Error logging to a file

Optimization (20 points):

  • (10 pts) Optimized temperature (0.3 for support)
  • (10 pts) Limited max tokens (150)

Cost Tracking (10 points):

  • (10 pts) Cost calculated correctly

Total: ___/100 points

Interpretation:

  • 90-100: ✅ Excellent
  • 70-89: ⚠️ Good
  • <70: ❌ Review

🎯 Extensions (Optional)

1. Persistent memory across sessions:

# On startup, load the previous conversation
def load_last_conversation(self):
    # Find the latest JSON
    # Load the 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 with Streamlit:

pip install streamlit
import streamlit as st

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

📊 Summary

What you built:

  1. Production-ready chatbot:

    • Conversational context
    • Custom system message
    • Optimized parameters
  2. Robust error handling:

    • Retry with exponential backoff
    • Logging to a file
    • Graceful degradation
  3. Cost tracking:

    • Automatic calculation
    • Conversation analysis
  4. Persistence:

    • JSON per conversation
    • Later analysis

Skills mastered:

  • ✅ Full OpenAI SDK
  • ✅ Conversations with context
  • ✅ Error handling in production
  • ✅ Cost optimization
  • ✅ Logging and debugging

Congratulations! You completed Module 2.


🔗 Additional resources

  1. OpenAI Best Practices
  2. Streamlit Docs - For a web UI
  3. Prompt Engineering - Improve system messages

➡️ Next step

Next module: Module 3 - LM Studio (Local GUI)

You'll learn to run LLMs locally on your machine:

  • Zero operating cost
  • 100% privacy
  • OpenAI-compatible API (reusable code!)

Time: 2-3 hours


Estimated time: 60-90 minutes
Project completed! 🎉