Building a Simple Chatbot

This tutorial walks you through building a complete chatbot from scratch using Arshai’s core framework. You’ll learn how to create a conversational agent with memory, handle user interactions, and deploy it as a working application.

What You’ll Build:

  • A conversational chatbot with memory

  • Command handling (help, clear, quit)

  • Conversation history tracking

  • Simple web interface (optional)

What You’ll Learn:

  • Direct instantiation of framework components

  • Memory integration for conversations

  • Agent design patterns

  • Error handling and user experience

Prerequisites:

  • Python 3.9+

  • Arshai installed: pip install arshai[openai]

  • OpenAI API key

Time to Complete: 30-45 minutes

Project Setup

Create Project Structure

# Create project directory
mkdir simple-chatbot
cd simple-chatbot

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install arshai[openai] python-dotenv

# Create project files
touch .env
touch chatbot.py
touch requirements.txt

Configure Environment

Create .env file:

# .env
OPENAI_API_KEY=your-api-key-here

Create requirements.txt:

arshai[openai]
python-dotenv

Step 1: Build the Core Chatbot Agent

Create the main chatbot agent:

# chatbot.py
import asyncio
import os
from datetime import datetime
from typing import Dict, Any
from dotenv import load_dotenv

from arshai.llms.openai import OpenAIClient
from arshai.core.interfaces.illm import ILLMConfig, ILLMInput
from arshai.agents.base import BaseAgent
from arshai.core.interfaces.iagent import IAgentInput
from arshai.memory.working_memory.in_memory_manager import InMemoryManager
from arshai.core.interfaces.imemorymanager import IMemoryInput, IWorkingMemory
from arshai.memory.memory_types import ConversationMemoryType

# Load environment variables
load_dotenv()

class SimpleChatbot(BaseAgent):
    """A simple chatbot with memory and conversation management."""

    def __init__(self, llm_client, memory_manager, conversation_id: str):
        super().__init__(
            llm_client,
            system_prompt="""You are a helpful and friendly AI assistant.
            You remember previous conversations and provide personalized responses.
            Keep your answers concise but informative."""
        )
        self.memory_manager = memory_manager
        self.conversation_id = conversation_id
        self.interaction_count = 0

    async def process(self, input: IAgentInput) -> Dict[str, Any]:
        """Process user message with memory."""
        self.interaction_count += 1

        # Retrieve conversation memory
        memory_context = self._get_conversation_memory()

        # Build enhanced prompt with memory
        enhanced_prompt = self._build_prompt_with_memory(
            input.message,
            memory_context
        )

        # Call LLM
        llm_input = ILLMInput(
            system_prompt=self.system_prompt,
            user_message=enhanced_prompt
        )

        result = await self.llm_client.chat(llm_input)
        response = result.get('llm_response', '')

        # Store conversation turn in memory
        self._store_conversation_turn(input.message, response)

        return {
            "response": response,
            "interaction_count": self.interaction_count,
            "timestamp": datetime.now().isoformat()
        }

    def _get_conversation_memory(self) -> str:
        """Retrieve conversation history from memory."""
        try:
            memory_input = IMemoryInput(
                conversation_id=self.conversation_id,
                memory_type=ConversationMemoryType.WORKING_MEMORY
            )

            memories = self.memory_manager.retrieve(memory_input)

            if memories:
                return memories[0].working_memory
            return ""

        except Exception as e:
            print(f"Error retrieving memory: {e}")
            return ""

    def _store_conversation_turn(self, user_message: str, bot_response: str):
        """Store conversation turn in memory."""
        try:
            # Get existing memory
            existing_memory = self._get_conversation_memory()

            # Append new turn
            conversation_turn = f"\nUser: {user_message}\nAssistant: {bot_response}"

            # Keep last 10 turns to avoid token limits
            memory_lines = existing_memory.split('\n')
            if len(memory_lines) > 40:  # 10 turns * 4 lines average
                memory_lines = memory_lines[-40:]

            updated_memory = '\n'.join(memory_lines) + conversation_turn

            # Store updated memory
            memory_input = IMemoryInput(
                conversation_id=self.conversation_id,
                memory_type=ConversationMemoryType.WORKING_MEMORY,
                data=[IWorkingMemory(working_memory=updated_memory)]
            )

            self.memory_manager.store(memory_input)

        except Exception as e:
            print(f"Error storing memory: {e}")

    def _build_prompt_with_memory(self, current_message: str, memory: str) -> str:
        """Build prompt that includes conversation history."""
        if memory:
            return f"""Previous conversation:
{memory}

Current message: {current_message}"""
        return current_message

    def clear_memory(self):
        """Clear conversation memory."""
        try:
            memory_input = IMemoryInput(
                conversation_id=self.conversation_id,
                memory_type=ConversationMemoryType.WORKING_MEMORY
            )
            self.memory_manager.delete(memory_input)
            self.interaction_count = 0
            print("✓ Conversation memory cleared")
        except Exception as e:
            print(f"Error clearing memory: {e}")

Step 2: Build the CLI Interface

Add command handling and user interface:

class ChatbotCLI:
    """Command-line interface for the chatbot."""

    def __init__(self, chatbot: SimpleChatbot):
        self.chatbot = chatbot
        self.running = False

    def print_welcome(self):
        """Print welcome message."""
        print("\n" + "=" * 60)
        print("🤖 Simple Chatbot")
        print("=" * 60)
        print("\nCommands:")
        print("  /help   - Show this help message")
        print("  /clear  - Clear conversation history")
        print("  /quit   - Exit the chatbot")
        print("\nStart chatting! Type your message and press Enter.\n")

    def print_help(self):
        """Print help message."""
        print("\n📖 Available Commands:")
        print("  /help   - Show available commands")
        print("  /clear  - Clear conversation memory")
        print("  /quit   - Exit the application")
        print("\nJust type normally to chat with the bot!\n")

    async def handle_message(self, user_input: str):
        """Handle user message."""
        try:
            # Show typing indicator
            print("🤖 Bot: ", end="", flush=True)

            # Process message
            result = await self.chatbot.process(
                IAgentInput(message=user_input)
            )

            # Display response
            print(result['response'])

            # Show metadata (optional)
            if result.get('interaction_count'):
                print(f"\n💬 Turn {result['interaction_count']}", end=" ")
                print(f"• {result['timestamp'][:19]}")

        except Exception as e:
            print(f"\n❌ Error: {e}")
            print("Please try again.\n")

    async def run(self):
        """Run the chatbot CLI."""
        self.running = True
        self.print_welcome()

        while self.running:
            try:
                # Get user input
                user_input = input("\n👤 You: ").strip()

                # Handle empty input
                if not user_input:
                    continue

                # Handle commands
                if user_input.startswith('/'):
                    await self.handle_command(user_input.lower())
                    continue

                # Handle regular message
                await self.handle_message(user_input)

            except KeyboardInterrupt:
                print("\n\n👋 Goodbye!")
                self.running = False
            except EOFError:
                print("\n\n👋 Goodbye!")
                self.running = False
            except Exception as e:
                print(f"\n❌ Unexpected error: {e}")

    async def handle_command(self, command: str):
        """Handle chatbot commands."""
        if command == '/quit':
            print("\n👋 Goodbye!")
            self.running = False

        elif command == '/clear':
            self.chatbot.clear_memory()

        elif command == '/help':
            self.print_help()

        else:
            print(f"❌ Unknown command: {command}")
            print("Type /help for available commands")

Step 3: Create the Main Application

Wire everything together:

async def create_chatbot(conversation_id: str = None) -> SimpleChatbot:
    """Create and configure the chatbot."""

    # Generate conversation ID if not provided
    if conversation_id is None:
        from uuid import uuid4
        conversation_id = f"chat_{uuid4().hex[:8]}"

    # Check for API key
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        raise ValueError(
            "OPENAI_API_KEY not found. "
            "Please set it in your .env file or environment."
        )

    # Create LLM client
    llm_config = ILLMConfig(
        model="gpt-3.5-turbo",
        temperature=0.7,
        max_tokens=500
    )
    llm_client = OpenAIClient(llm_config)

    # Create memory manager
    memory_manager = InMemoryManager(ttl=3600)  # 1 hour TTL

    # Create chatbot
    chatbot = SimpleChatbot(
        llm_client=llm_client,
        memory_manager=memory_manager,
        conversation_id=conversation_id
    )

    return chatbot

async def main():
    """Main application entry point."""
    try:
        # Create chatbot
        chatbot = await create_chatbot()

        # Create and run CLI
        cli = ChatbotCLI(chatbot)
        await cli.run()

    except ValueError as e:
        print(f"❌ Configuration Error: {e}")
    except Exception as e:
        print(f"❌ Error starting chatbot: {e}")

if __name__ == "__main__":
    asyncio.run(main())

Step 4: Run and Test

Run the chatbot:

python chatbot.py

Test conversation:

🤖 Simple Chatbot
================================================================

Commands:
  /help   - Show this help message
  /clear  - Clear conversation history
  /quit   - Exit the chatbot

Start chatting! Type your message and press Enter.

👤 You: Hello! My name is Alice.
🤖 Bot: Hello Alice! It's nice to meet you. How can I help you today?

💬 Turn 1 • 2024-01-15 10:30:45

👤 You: What's my name?
🤖 Bot: Your name is Alice, as you just told me!

💬 Turn 2 • 2024-01-15 10:30:52

👤 You: /clear
✓ Conversation memory cleared

👤 You: What's my name?
🤖 Bot: I don't know your name yet. Would you like to tell me?

💬 Turn 1 • 2024-01-15 10:31:05

👤 You: /quit
👋 Goodbye!

Step 5: Add Enhancements (Optional)

Enhance with Typing Indicator

import sys
import time
import threading

def show_typing_indicator():
    """Show animated typing indicator."""
    chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
    for _ in range(10):
        for char in chars:
            sys.stdout.write(f'\r🤖 Bot: {char} thinking...')
            sys.stdout.flush()
            time.sleep(0.1)
    sys.stdout.write('\r' + ' ' * 40 + '\r')
    sys.stdout.flush()

# Use in handle_message:
# threading.Thread(target=show_typing_indicator, daemon=True).start()

Add Conversation Export

def export_conversation(self, filename: str = None):
    """Export conversation to file."""
    if filename is None:
        filename = f"conversation_{self.conversation_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"

    memory = self._get_conversation_memory()

    with open(filename, 'w') as f:
        f.write(f"Conversation Export\n")
        f.write(f"ID: {self.conversation_id}\n")
        f.write(f"Date: {datetime.now().isoformat()}\n")
        f.write(f"Turns: {self.interaction_count}\n")
        f.write("=" * 60 + "\n\n")
        f.write(memory)

    print(f"✓ Conversation exported to {filename}")

# Add command in handle_command:
elif command == '/export':
    self.chatbot.export_conversation()

Add Conversation Statistics

def get_statistics(self) -> Dict[str, Any]:
    """Get conversation statistics."""
    memory = self._get_conversation_memory()

    return {
        "conversation_id": self.conversation_id,
        "total_turns": self.interaction_count,
        "memory_size": len(memory),
        "memory_lines": len(memory.split('\n')) if memory else 0
    }

# Add command:
elif command == '/stats':
    stats = self.chatbot.get_statistics()
    print("\n📊 Conversation Statistics:")
    print(f"  ID: {stats['conversation_id']}")
    print(f"  Turns: {stats['total_turns']}")
    print(f"  Memory Size: {stats['memory_size']} characters")
    print(f"  Memory Lines: {stats['memory_lines']}")

Step 6: Add Web Interface (Optional)

Create simple web interface using Flask:

# web_chatbot.py
from flask import Flask, render_template, request, jsonify
import asyncio

app = Flask(__name__)
chatbot = None

@app.route('/')
def index():
    return render_template('chat.html')

@app.route('/chat', methods=['POST'])
def chat():
    user_message = request.json.get('message')

    # Process message
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    result = loop.run_until_complete(
        chatbot.process(IAgentInput(message=user_message))
    )

    return jsonify(result)

@app.route('/clear', methods=['POST'])
def clear():
    chatbot.clear_memory()
    return jsonify({"status": "cleared"})

if __name__ == '__main__':
    # Initialize chatbot
    loop = asyncio.get_event_loop()
    chatbot = loop.run_until_complete(create_chatbot("web_session"))

    app.run(debug=True, port=5000)

Create templates/chat.html:

<!DOCTYPE html>
<html>
<head>
    <title>Simple Chatbot</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; }
        #chat-container { border: 1px solid #ccc; height: 400px; overflow-y: auto; padding: 10px; }
        .message { margin: 10px 0; padding: 10px; border-radius: 5px; }
        .user { background: #e3f2fd; text-align: right; }
        .bot { background: #f5f5f5; }
        #input-container { margin-top: 10px; }
        input { width: 80%; padding: 10px; }
        button { padding: 10px 20px; }
    </style>
</head>
<body>
    <h1>🤖 Simple Chatbot</h1>
    <div id="chat-container"></div>
    <div id="input-container">
        <input type="text" id="user-input" placeholder="Type your message...">
        <button onclick="sendMessage()">Send</button>
        <button onclick="clearChat()">Clear</button>
    </div>

    <script>
        async function sendMessage() {
            const input = document.getElementById('user-input');
            const message = input.value.trim();
            if (!message) return;

            addMessage('user', message);
            input.value = '';

            const response = await fetch('/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({message: message})
            });

            const data = await response.json();
            addMessage('bot', data.response);
        }

        function addMessage(type, text) {
            const container = document.getElementById('chat-container');
            const div = document.createElement('div');
            div.className = `message ${type}`;
            div.textContent = text;
            container.appendChild(div);
            container.scrollTop = container.scrollHeight;
        }

        async function clearChat() {
            await fetch('/clear', {method: 'POST'});
            document.getElementById('chat-container').innerHTML = '';
        }

        document.getElementById('user-input').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') sendMessage();
        });
    </script>
</body>
</html>

Production Considerations

Error Handling

Add robust error handling:

class RobustChatbot(SimpleChatbot):
    """Chatbot with enhanced error handling."""

    async def process(self, input: IAgentInput) -> Dict[str, Any]:
        max_retries = 3

        for attempt in range(max_retries):
            try:
                return await super().process(input)

            except Exception as e:
                if attempt == max_retries - 1:
                    return {
                        "response": "I'm having trouble processing your request. Please try again.",
                        "error": str(e),
                        "interaction_count": self.interaction_count
                    }

                await asyncio.sleep(1)  # Wait before retry

Rate Limiting

from datetime import datetime, timedelta

class RateLimitedChatbot(SimpleChatbot):
    """Chatbot with rate limiting."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.request_times = []
        self.max_requests = 10  # per minute

    async def process(self, input: IAgentInput) -> Dict[str, Any]:
        # Check rate limit
        now = datetime.now()
        self.request_times = [
            t for t in self.request_times
            if now - t < timedelta(minutes=1)
        ]

        if len(self.request_times) >= self.max_requests:
            return {
                "response": "Rate limit exceeded. Please wait a moment.",
                "error": "rate_limit"
            }

        self.request_times.append(now)
        return await super().process(input)

Logging

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('chatbot.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger('SimpleChatbot')

# Use in chatbot:
logger.info(f"Processing message for conversation {self.conversation_id}")
logger.error(f"Error: {e}", exc_info=True)

Next Steps

Enhance the chatbot:

  • Add personality customization

  • Implement sentiment analysis

  • Add multi-language support

  • Integrate external APIs as tools

Scale the application:

  • Use Redis for persistent memory: Redis Memory Manager

  • Add user authentication

  • Deploy to cloud platforms

  • Implement conversation analytics

Learn more:

Congratulations! You’ve built a complete chatbot with Arshai’s framework. 🎉