ai-agenttutorialassistantarchitectureopenainodejs

Build a Personal AI Assistant - Part 2: Core Assistant Architecture & Logic

By AgentForge Hub8/14/202518 min read
Intermediate
Build a Personal AI Assistant - Part 2: Core Assistant Architecture & Logic

Ad Space

Build a Personal AI Assistant - Part 2: Core Assistant Architecture & Logic

Now we'll architect the brain of your AI assistant. This isn't just about connecting to OpenAIβ€”we're building a robust, scalable system that handles failures gracefully, manages resources efficiently, and provides a delightful user experience.


Tutorial Navigation


What You'll Understand & Build

By the end of this tutorial, you'll understand:

  • 🧠 How to architect resilient AI systems
  • πŸ”„ Why retry logic prevents user frustration
  • ⚑ How rate limiting saves money and prevents bans
  • 🎭 Why prompt engineering shapes AI behavior
  • πŸ§ͺ How testing ensures reliability
  • πŸ’¬ Why CLI interfaces accelerate development

Estimated Time: 25 minutes


The Resilient AI Assistant Architecture

Our assistant follows a layered architecture where each component has a specific responsibility:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   CLI Interface │───▢│  Assistant Core │───▢│   OpenAI API    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                       β”‚  Configuration  β”‚
                       β”‚   & Logging     β”‚
                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This architecture provides:

  • Separation of concerns - UI, logic, and external services are independent
  • Testability - Each layer can be tested in isolation
  • Flexibility - Switch AI providers without changing the core logic
  • Observability - Centralized logging and configuration management

Configuration Management: The Foundation of Flexibility

Why Configuration Matters

Configuration isn't just about storing API keysβ€”it's about operational flexibility:

  • Environment-specific behavior - Different settings for dev/staging/production
  • Feature flags - Enable/disable features without code changes
  • Performance tuning - Adjust rate limits and timeouts based on usage
  • Personality customization - Different assistant personalities for different use cases

The Validation Strategy

Configuration validation prevents runtime surprises. Instead of discovering a misconfigured temperature setting during an important conversation, we catch it at startup.

Create src/config/assistant.js

import dotenv from 'dotenv';

dotenv.config();

export const config = {
  // OpenAI Configuration
  openai: {
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
    maxTokens: parseInt(process.env.MAX_TOKENS) || 150,
    temperature: parseFloat(process.env.TEMPERATURE) || 0.7,
    timeout: parseInt(process.env.API_TIMEOUT) || 30000,
  },

  // Rate Limiting
  rateLimiting: {
    maxRequestsPerMinute: parseInt(process.env.MAX_REQUESTS_PER_MINUTE) || 20,
    maxTokensPerMinute: parseInt(process.env.MAX_TOKENS_PER_MINUTE) || 40000,
  },

  // Retry Configuration
  retry: {
    maxAttempts: parseInt(process.env.MAX_RETRY_ATTEMPTS) || 3,
    baseDelay: parseInt(process.env.RETRY_BASE_DELAY) || 1000,
    maxDelay: parseInt(process.env.RETRY_MAX_DELAY) || 10000,
  },

  // Assistant Personality
  personality: {
    name: process.env.ASSISTANT_NAME || 'Alex',
    role: process.env.ASSISTANT_ROLE || 'helpful assistant',
    tone: process.env.ASSISTANT_TONE || 'friendly and professional',
  },

  // Logging
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    saveToFile: process.env.SAVE_LOGS === 'true',
  },
};

// Validate required configuration
export function validateConfig() {
  const errors = [];

  if (!config.openai.apiKey) {
    errors.push('OPENAI_API_KEY is required');
  }

  if (config.openai.temperature < 0 || config.openai.temperature > 2) {
    errors.push('TEMPERATURE must be between 0 and 2');
  }

  if (config.openai.maxTokens < 1 || config.openai.maxTokens > 4096) {
    errors.push('MAX_TOKENS must be between 1 and 4096');
  }

  if (errors.length > 0) {
    throw new Error(`Configuration errors:\n${errors.join('\n')}`);
  }

  return true;
}

Enhanced .env Configuration

# AI Model Configuration
OPENAI_MODEL=gpt-3.5-turbo
MAX_TOKENS=150
TEMPERATURE=0.7
API_TIMEOUT=30000

# Rate Limiting
MAX_REQUESTS_PER_MINUTE=20
MAX_TOKENS_PER_MINUTE=40000

# Retry Logic
MAX_RETRY_ATTEMPTS=3
RETRY_BASE_DELAY=1000
RETRY_MAX_DELAY=10000

# Assistant Personality
ASSISTANT_NAME=Alex
ASSISTANT_ROLE=helpful personal assistant
ASSISTANT_TONE=friendly and professional

# Logging
SAVE_LOGS=true

Building the Core Assistant: A Resilient AI Brain

The Assistant Class Philosophy

The Assistant class is more than just an API wrapperβ€”it's the orchestration layer that brings together:

  • State management - Conversation history and metadata
  • Error resilience - Graceful failure handling and recovery
  • Performance monitoring - Real-time statistics and analytics
  • Resource management - Rate limiting and cost control

Why This Architecture Works

Each method in our Assistant class has a single responsibility:

  • getResponse() - Orchestrates the entire conversation flow
  • callOpenAI() - Handles pure API communication
  • buildSystemPrompt() - Manages AI personality and behavior
  • updateStats() - Tracks performance metrics

This separation makes the code testable, maintainable, and extensible.

Create src/services/Assistant.js

import OpenAI from 'openai';
import { config, validateConfig } from '../config/assistant.js';
import { createLogger } from '../utils/logger.js';
import { RateLimiter } from '../utils/rateLimiter.js';
import { RetryHandler } from '../utils/retryHandler.js';

export class Assistant {
  constructor(options = {}) {
    // Validate configuration
    validateConfig();

    // Initialize logger
    this.logger = createLogger();

    // Initialize OpenAI client
    this.openai = new OpenAI({
      apiKey: config.openai.apiKey,
      timeout: config.openai.timeout,
    });

    // Initialize rate limiter
    this.rateLimiter = new RateLimiter({
      maxRequests: config.rateLimiting.maxRequestsPerMinute,
      windowMs: 60000, // 1 minute
    });

    // Initialize retry handler
    this.retryHandler = new RetryHandler({
      maxAttempts: config.retry.maxAttempts,
      baseDelay: config.retry.baseDelay,
      maxDelay: config.retry.maxDelay,
    });

    // Assistant state
    this.conversationHistory = [];
    this.isInitialized = false;
    this.stats = {
      totalRequests: 0,
      totalTokensUsed: 0,
      averageResponseTime: 0,
      errors: 0,
    };

    this.logger.info('πŸ€– Assistant initialized successfully');
  }

  /**
   * Initialize the assistant with system prompt
   */
  async initialize() {
    if (this.isInitialized) {
      return;
    }

    const systemPrompt = this.buildSystemPrompt();
    
    this.conversationHistory = [
      {
        role: 'system',
        content: systemPrompt,
        timestamp: new Date().toISOString(),
      },
    ];

    this.isInitialized = true;
    this.logger.info('βœ… Assistant initialized with system prompt');
  }

  /**
   * Generate response to user input
   */
  async getResponse(userInput, options = {}) {
    try {
      // Ensure assistant is initialized
      await this.initialize();

      // Validate input
      if (!userInput || typeof userInput !== 'string') {
        throw new Error('User input must be a non-empty string');
      }

      if (userInput.trim().length === 0) {
        throw new Error('User input cannot be empty');
      }

      // Check rate limits
      await this.rateLimiter.checkLimit();

      // Start timing
      const startTime = Date.now();

      // Add user message to history
      const userMessage = {
        role: 'user',
        content: userInput.trim(),
        timestamp: new Date().toISOString(),
      };

      this.conversationHistory.push(userMessage);
      this.logger.info(`πŸ‘€ User: ${userInput}`);

      // Generate response with retry logic
      const response = await this.retryHandler.execute(
        () => this.callOpenAI(options)
      );

      // Add assistant response to history
      const assistantMessage = {
        role: 'assistant',
        content: response.content,
        timestamp: new Date().toISOString(),
        metadata: {
          model: response.model,
          tokensUsed: response.usage?.total_tokens || 0,
          responseTime: Date.now() - startTime,
        },
      };

      this.conversationHistory.push(assistantMessage);

      // Update statistics
      this.updateStats(assistantMessage.metadata);

      this.logger.info(`πŸ€– ${config.personality.name}: ${response.content}`);

      return {
        content: response.content,
        metadata: assistantMessage.metadata,
        conversationId: this.generateConversationId(),
      };

    } catch (error) {
      this.stats.errors++;
      this.logger.error('Error generating response:', error);
      
      // Return user-friendly error message
      return {
        content: "I'm sorry, I encountered an error while processing your request. Please try again.",
        error: true,
        errorType: error.name,
        metadata: {
          responseTime: 0,
          tokensUsed: 0,
        },
      };
    }
  }

  /**
   * Call OpenAI API
   */
  async callOpenAI(options = {}) {
    const requestOptions = {
      model: options.model || config.openai.model,
      messages: this.conversationHistory,
      max_tokens: options.maxTokens || config.openai.maxTokens,
      temperature: options.temperature || config.openai.temperature,
      ...options,
    };

    this.logger.debug('Sending request to OpenAI:', {
      model: requestOptions.model,
      messageCount: requestOptions.messages.length,
      maxTokens: requestOptions.max_tokens,
    });

    const completion = await this.openai.chat.completions.create(requestOptions);

    const response = {
      content: completion.choices[0].message.content,
      model: completion.model,
      usage: completion.usage,
    };

    this.logger.debug('Received response from OpenAI:', {
      tokensUsed: response.usage?.total_tokens,
      model: response.model,
    });

    return response;
  }

  /**
   * Build system prompt based on configuration
   */
  buildSystemPrompt() {
    return `You are ${config.personality.name}, a ${config.personality.role}.

Your personality traits:
- Tone: ${config.personality.tone}
- Always be helpful and accurate
- If you don't know something, admit it
- Keep responses concise but informative
- Use emojis sparingly and appropriately

Current date and time: ${new Date().toLocaleString()}

Remember to stay in character and provide helpful, accurate responses.`;
  }

  /**
   * Update statistics
   */
  updateStats(metadata) {
    this.stats.totalRequests++;
    this.stats.totalTokensUsed += metadata.tokensUsed;
    
    // Calculate running average response time
    const currentAvg = this.stats.averageResponseTime;
    const newAvg = (currentAvg * (this.stats.totalRequests - 1) + metadata.responseTime) / this.stats.totalRequests;
    this.stats.averageResponseTime = Math.round(newAvg);
  }

  /**
   * Generate conversation ID
   */
  generateConversationId() {
    return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  /**
   * Clear conversation history
   */
  clearHistory() {
    this.conversationHistory = [];
    this.isInitialized = false;
    this.logger.info('πŸ—‘οΈ Conversation history cleared');
  }

  /**
   * Get conversation statistics
   */
  getStats() {
    return {
      ...this.stats,
      conversationLength: this.conversationHistory.length,
      isInitialized: this.isInitialized,
    };
  }

  /**
   * Get conversation history
   */
  getHistory() {
    return this.conversationHistory.filter(msg => msg.role !== 'system');
  }

  /**
   * Export conversation for analysis
   */
  exportConversation() {
    return {
      timestamp: new Date().toISOString(),
      config: {
        model: config.openai.model,
        temperature: config.openai.temperature,
        maxTokens: config.openai.maxTokens,
      },
      conversation: this.getHistory(),
      stats: this.getStats(),
    };
  }
}

Resilience Utilities: Rate Limiting & Retry Logic

Why Rate Limiting Matters

Rate limiting isn't just about avoiding API bansβ€”it's about cost management:

  • Prevents runaway costs from accidental loops or spam
  • Ensures fair resource usage in multi-user scenarios
  • Provides predictable performance characteristics
  • Enables graceful degradation under high load

The Exponential Backoff Strategy

Our retry handler uses exponential backoff because:

  • Reduces server load during outages
  • Maximizes success rate for transient failures
  • Prevents thundering herd problems
  • Respects service recovery time patterns

Create src/utils/rateLimiter.js

export class RateLimiter {
  constructor(options = {}) {
    this.maxRequests = options.maxRequests || 20;
    this.windowMs = options.windowMs || 60000; // 1 minute
    this.requests = [];
  }

  async checkLimit() {
    const now = Date.now();
    
    // Remove old requests outside the window
    this.requests = this.requests.filter(
      timestamp => now - timestamp < this.windowMs
    );

    // Check if we're at the limit
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = Math.min(...this.requests);
      const waitTime = this.windowMs - (now - oldestRequest);
      
      throw new Error(
        `Rate limit exceeded. Please wait ${Math.ceil(waitTime / 1000)} seconds.`
      );
    }

    // Add current request
    this.requests.push(now);
  }

  getRemainingRequests() {
    const now = Date.now();
    const recentRequests = this.requests.filter(
      timestamp => now - timestamp < this.windowMs
    );
    
    return Math.max(0, this.maxRequests - recentRequests.length);
  }

  getResetTime() {
    if (this.requests.length === 0) {
      return 0;
    }

    const oldestRequest = Math.min(...this.requests);
    const resetTime = oldestRequest + this.windowMs;
    
    return Math.max(0, resetTime - Date.now());
  }
}

Create src/utils/retryHandler.js

export class RetryHandler {
  constructor(options = {}) {
    this.maxAttempts = options.maxAttempts || 3;
    this.baseDelay = options.baseDelay || 1000;
    this.maxDelay = options.maxDelay || 10000;
  }

  async execute(fn, attempt = 1) {
    try {
      return await fn();
    } catch (error) {
      // Don't retry on certain errors
      if (this.shouldNotRetry(error) || attempt >= this.maxAttempts) {
        throw error;
      }

      // Calculate delay with exponential backoff
      const delay = Math.min(
        this.baseDelay * Math.pow(2, attempt - 1),
        this.maxDelay
      );

      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      
      await this.sleep(delay);
      return this.execute(fn, attempt + 1);
    }
  }

  shouldNotRetry(error) {
    // Don't retry on authentication errors or invalid requests
    const nonRetryableErrors = [
      'Authentication',
      'Authorization', 
      'InvalidRequest',
      'ValidationError',
    ];

    return nonRetryableErrors.some(type => 
      error.name?.includes(type) || error.message?.includes(type.toLowerCase())
    );
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Step 4: Create Interactive CLI Interface

Now let's create a user-friendly CLI interface to interact with our assistant.

Create src/cli/interactive.js

import readline from 'readline';
import chalk from 'chalk';
import { Assistant } from '../services/Assistant.js';
import { createLogger } from '../utils/logger.js';

export class InteractiveCLI {
  constructor() {
    this.assistant = new Assistant();
    this.logger = createLogger();
    this.rl = null;
    this.isRunning = false;
  }

  async start() {
    try {
      // Initialize assistant
      await this.assistant.initialize();

      // Setup readline interface
      this.rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
        prompt: chalk.blue('You: '),
      });

      // Display welcome message
      this.displayWelcome();

      // Setup event listeners
      this.setupEventListeners();

      // Start the conversation
      this.isRunning = true;
      this.rl.prompt();

    } catch (error) {
      console.error(chalk.red('Failed to start CLI:'), error.message);
      process.exit(1);
    }
  }

  setupEventListeners() {
    this.rl.on('line', async (input) => {
      await this.handleUserInput(input.trim());
    });

    this.rl.on('close', () => {
      this.handleExit();
    });

    // Handle Ctrl+C
    process.on('SIGINT', () => {
      this.handleExit();
    });
  }

  async handleUserInput(input) {
    if (!input) {
      this.rl.prompt();
      return;
    }

    // Handle special commands
    if (input.startsWith('/')) {
      await this.handleCommand(input);
      return;
    }

    try {
      // Show thinking indicator
      const thinkingInterval = this.showThinking();

      // Get response from assistant
      const response = await this.assistant.getResponse(input);

      // Stop thinking indicator
      clearInterval(thinkingInterval);
      process.stdout.write('\r\x1b[K'); // Clear line

      // Display response
      if (response.error) {
        console.log(chalk.red(`❌ Error: ${response.content}`));
      } else {
        console.log(chalk.green(`πŸ€– Alex: ${response.content}`));
        
        // Show metadata in debug mode
        if (process.env.LOG_LEVEL === 'debug') {
          console.log(chalk.gray(`   ⏱️ ${response.metadata.responseTime}ms | 🎯 ${response.metadata.tokensUsed} tokens`));
        }
      }

    } catch (error) {
      console.log(chalk.red(`❌ Error: ${error.message}`));
    }

    console.log(); // Add spacing
    this.rl.prompt();
  }

  async handleCommand(command) {
    const [cmd, ...args] = command.slice(1).split(' ');

    switch (cmd.toLowerCase()) {
      case 'help':
        this.displayHelp();
        break;

      case 'stats':
        this.displayStats();
        break;

      case 'history':
        this.displayHistory();
        break;

      case 'clear':
        this.assistant.clearHistory();
        console.log(chalk.yellow('πŸ—‘οΈ Conversation history cleared'));
        break;

      case 'export':
        this.exportConversation();
        break;

      case 'settings':
        this.displaySettings();
        break;

      case 'quit':
      case 'exit':
        this.handleExit();
        return;

      default:
        console.log(chalk.red(`Unknown command: ${cmd}`));
        console.log(chalk.gray('Type /help for available commands'));
    }

    console.log();
    this.rl.prompt();
  }

  showThinking() {
    const frames = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'];
    let i = 0;
    
    return setInterval(() => {
      process.stdout.write(`\r${chalk.blue(frames[i % frames.length])} Thinking...`);
      i++;
    }, 100);
  }

  displayWelcome() {
    console.log(chalk.cyan('╔══════════════════════════════════════════════════════════════════════╗'));
    console.log(chalk.cyan('β•‘                    πŸ€– Personal AI Assistant                         β•‘'));
    console.log(chalk.cyan('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•'));
    console.log();
    console.log(chalk.green('Welcome! I\'m Alex, your personal AI assistant.'));
    console.log(chalk.gray('Type your questions or use commands like /help, /stats, /quit'));
    console.log(chalk.gray('Press Ctrl+C or type /quit to exit'));
    console.log();
  }

  displayHelp() {
    console.log(chalk.cyan('Available Commands:'));
    console.log(chalk.yellow('/help     ') + '- Show this help message');
    console.log(chalk.yellow('/stats    ') + '- Display conversation statistics');
    console.log(chalk.yellow('/history  ') + '- Show conversation history');
    console.log(chalk.yellow('/clear    ') + '- Clear conversation history');
    console.log(chalk.yellow('/export   ') + '- Export conversation to file');
    console.log(chalk.yellow('/settings ') + '- Show current settings');
    console.log(chalk.yellow('/quit     ') + '- Exit the assistant');
  }

  displayStats() {
    const stats = this.assistant.getStats();
    
    console.log(chalk.cyan('πŸ“Š Conversation Statistics:'));
    console.log(`   πŸ’¬ Total requests: ${stats.totalRequests}`);
    console.log(`   🎯 Total tokens used: ${stats.totalTokensUsed}`);
    console.log(`   ⏱️ Average response time: ${stats.averageResponseTime}ms`);
    console.log(`   πŸ“ Conversation length: ${stats.conversationLength} messages`);
    console.log(`   ❌ Errors: ${stats.errors}`);
  }

  displayHistory() {
    const history = this.assistant.getHistory();
    
    if (history.length === 0) {
      console.log(chalk.gray('No conversation history yet.'));
      return;
    }

    console.log(chalk.cyan('πŸ“– Conversation History:'));
    history.forEach((message, index) => {
      const role = message.role === 'user' ? 'πŸ‘€ You' : 'πŸ€– Alex';
      const color = message.role === 'user' ? chalk.blue : chalk.green;
      console.log(color(`${index + 1}. ${role}: ${message.content}`));
    });
  }

  displaySettings() {
    console.log(chalk.cyan('βš™οΈ Current Settings:'));
    console.log(`   πŸ€– Model: ${process.env.OPENAI_MODEL || 'gpt-3.5-turbo'}`);
    console.log(`   🌑️ Temperature: ${process.env.TEMPERATURE || '0.7'}`);
    console.log(`   πŸ“ Max tokens: ${process.env.MAX_TOKENS || '150'}`);
    console.log(`   🎭 Assistant name: ${process.env.ASSISTANT_NAME || 'Alex'}`);
  }

  exportConversation() {
    try {
      const conversation = this.assistant.exportConversation();
      const filename = `conversation_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
      
      // In a real implementation, you'd save to file
      console.log(chalk.green(`πŸ“„ Conversation exported to: ${filename}`));
      console.log(chalk.gray('(Export functionality would save to file in production)'));
      
    } catch (error) {
      console.log(chalk.red(`❌ Export failed: ${error.message}`));
    }
  }

  handleExit() {
    if (this.isRunning) {
      console.log(chalk.yellow('\nπŸ‘‹ Thanks for chatting! Goodbye!'));
      
      // Display final stats
      const stats = this.assistant.getStats();
      if (stats.totalRequests > 0) {
        console.log(chalk.gray(`Final stats: ${stats.totalRequests} requests, ${stats.totalTokensUsed} tokens used`));
      }
    }

    if (this.rl) {
      this.rl.close();
    }

    this.isRunning = false;
    process.exit(0);
  }
}

Step 5: Install Additional Dependencies

We need a few more packages for our enhanced CLI:

npm install chalk

Step 6: Update Main Application File

Let's update our main file to use the new interactive CLI.

Update src/index.js

import dotenv from 'dotenv';
import { InteractiveCLI } from './cli/interactive.js';
import { createLogger } from './utils/logger.js';

// Load environment variables
dotenv.config();

// Initialize logger
const logger = createLogger();

async function main() {
  try {
    logger.info('πŸš€ Starting Personal AI Assistant...');
    
    // Create and start CLI
    const cli = new InteractiveCLI();
    await cli.start();
    
  } catch (error) {
    logger.error('πŸ’₯ Application failed to start:', error);
    process.exit(1);
  }
}

// Start the application
main();

Step 7: Create Tests

Let's add comprehensive tests for our Assistant class.

Create tests/Assistant.test.js

import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
import { Assistant } from '../src/services/Assistant.js';

// Mock OpenAI
jest.mock('openai', () => {
  return {
    __esModule: true,
    default: jest.fn().mockImplementation(() => ({
      chat: {
        completions: {
          create: jest.fn().mockResolvedValue({
            choices: [{ message: { content: 'Test response' } }],
            model: 'gpt-3.5-turbo',
            usage: { total_tokens: 10 }
          })
        }
      }
    }))
  };
});

describe('Assistant', () => {
  let assistant;

  beforeEach(() => {
    // Set required environment variables
    process.env.OPENAI_API_KEY = 'test-key';
    assistant = new Assistant();
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('Initialization', () => {
    test('should initialize successfully with valid config', () => {
      expect(assistant).toBeInstanceOf(Assistant);
      expect(assistant.isInitialized).toBe(false);
    });

    test('should initialize conversation with system prompt', async () => {
      await assistant.initialize();
      expect(assistant.isInitialized).toBe(true);
      expect(assistant.conversationHistory).toHaveLength(1);
      expect(assistant.conversationHistory[0].role).toBe('system');
    });
  });

  describe('Response Generation', () => {
    test('should generate response for valid input', async () => {
      const response = await assistant.getResponse('Hello');
      
      expect(response).toHaveProperty('content');
      expect(response).toHaveProperty('metadata');
      expect(response.content).toBe('Test response');
      expect(response.error).toBeUndefined();
    });

    test('should handle empty input', async () => {
      const response = await assistant.getResponse('');
      
      expect(response.error).toBe(true);
      expect(response.content).toContain('error');
    });

    test('should handle non-string input', async () => {
      const response = await assistant.getResponse(null);
      
      expect(response.error).toBe(true);
      expect(response.content).toContain('error');
    });
  });

  describe('Statistics', () => {
    test('should track conversation statistics', async () => {
      await assistant.getResponse('Test message');
      
      const stats = assistant.getStats();
      expect(stats.totalRequests).toBe(1);
      expect(stats.totalTokensUsed).toBe(10);
      expect(stats.errors).toBe(0);
    });

    test('should track errors in statistics', async () => {
      // Mock API failure
      assistant.openai.chat.completions.create.mockRejectedValueOnce(
        new Error('API Error')
      );
      
      await assistant.getResponse('Test message');
      
      const stats = assistant.getStats();
      expect(stats.errors).toBe(1);
    });
  });

  describe('History Management', () => {
    test('should maintain conversation history', async () => {
      await assistant.getResponse('Hello');
      await assistant.getResponse('How are you?');
      
      const history = assistant.getHistory();
      expect(history).toHaveLength(4); // 2 user + 2 assistant messages
      expect(history[0].role).toBe('user');
      expect(history[1].role).toBe('assistant');
    });

    test('should clear conversation history', async () => {
      await assistant.getResponse('Test message');
      assistant.clearHistory();
      
      expect(assistant.getHistory()).toHaveLength(0);
      expect(assistant.isInitialized).toBe(false);
    });
  });

  describe('Configuration', () => {
    test('should export conversation data', async () => {
      await assistant.getResponse('Test message');
      
      const exported = assistant.exportConversation();
      expect(exported).toHaveProperty('timestamp');
      expect(exported).toHaveProperty('config');
      expect(exported).toHaveProperty('conversation');
      expect(exported).toHaveProperty('stats');
    });
  });
});

Step 8: Testing Your Assistant

Let's test our assistant to make sure everything works correctly.

Run Your Tests

npm test

Expected output:

βœ“ Assistant β€Ί Initialization β€Ί should initialize successfully with valid config
βœ“ Assistant β€Ί Response Generation β€Ί should generate response for valid input
βœ“ All tests passed!

Test the Interactive CLI

npm run dev

You should see:

╔══════════════════════════════════════════════════════════════════════╗
β•‘                    πŸ€– Personal AI Assistant                         β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Welcome! I'm Alex, your personal AI assistant.
Type your questions or use commands like /help, /stats, /quit
Press Ctrl+C or type /quit to exit

You: 

Try These Test Commands

  1. Basic conversation:

    You: Hello, what's your name?
    πŸ€– Alex: Hello! My name is Alex, and I'm your personal AI assistant...
    
  2. Check statistics:

    You: /stats
    πŸ“Š Conversation Statistics:
       πŸ’¬ Total requests: 1
       🎯 Total tokens used: 15
       ⏱️ Average response time: 1250ms
    
  3. View history:

    You: /history
    πŸ“– Conversation History:
    1. πŸ‘€ You: Hello, what's your name?
    2. πŸ€– Alex: Hello! My name is Alex...
    

Troubleshooting Common Issues

OpenAI API Errors

Problem: Error: Incorrect API key provided Solution:

  • Verify your API key in .env file
  • Ensure no extra spaces or quotes around the key
  • Check that your OpenAI account has credits

Problem: Rate limit exceeded Solution:

  • Our rate limiter should handle this automatically
  • If persistent, increase RETRY_BASE_DELAY in .env

Module Import Errors

Problem: Cannot use import statement outside a module Solution:

  • Ensure "type": "module" is in your package.json
  • Use .js extensions in import statements

Memory Issues

Problem: Application runs out of memory Solution:

  • Clear conversation history regularly with /clear
  • Reduce MAX_TOKENS in .env
  • Implement conversation trimming (covered in Part 3)

Performance Optimization Tips

1. Token Management

  • Keep MAX_TOKENS reasonable (150-300 for chat)
  • Monitor token usage with /stats
  • Clear history when conversations get long

2. Response Time

  • Use gpt-3.5-turbo for faster responses
  • Implement conversation context trimming
  • Consider streaming responses for longer outputs

3. Cost Management

  • Set up usage monitoring
  • Use rate limiting effectively
  • Implement conversation archiving

Security Best Practices

1. API Key Protection

  • Never commit .env files to version control
  • Use environment variables in production
  • Rotate API keys regularly

2. Input Validation

  • Our Assistant class validates all inputs
  • Consider adding content filtering
  • Implement user authentication for production

3. Error Handling

  • Log errors securely (no sensitive data)
  • Provide user-friendly error messages
  • Implement proper retry logic

What We've Built

Congratulations! πŸŽ‰ You now have a sophisticated AI assistant with:

  • Core Assistant Class - Handles all AI interactions
  • Configuration Management - Centralized settings
  • Rate Limiting - Prevents API abuse
  • Retry Logic - Handles temporary failures
  • Interactive CLI - User-friendly interface
  • Comprehensive Testing - Ensures reliability
  • Error Handling - Graceful failure management
  • Statistics Tracking - Monitor usage and performance

Key Features Implemented

βœ… OpenAI Integration - Full GPT integration with error handling βœ… Conversation Management - History tracking and export βœ… Rate Limiting - Prevents API overuse βœ… Retry Logic - Handles temporary API failures βœ… Configuration - Flexible settings management βœ… CLI Interface - Interactive user experience βœ… Testing - Comprehensive test coverage βœ… Logging - Detailed logging and debugging


Next Steps

In Part 3: Conversation Memory, we'll add:

  • Persistent conversation storage
  • Context window management
  • Conversation summarization
  • Advanced memory techniques
  • Vector embeddings for semantic search

Quick Preview of Part 3

// Coming in Part 3
class ConversationMemory {
  async saveConversation(conversation) {
    // Save to database or file
  }
  
  async loadConversation(id) {
    // Load from storage
  }
  
  async summarizeConversation(messages) {
    // Create conversation summary
  }
}

Additional Resources


Ready to continue? Proceed to Part 3: Conversation Memory β†’

Your assistant is now ready for real conversations! Test it out and get familiar with the interface before moving to the next part.


Ad Space

Recommended Tools & Resources

* This section contains affiliate links. We may earn a commission when you purchase through these links at no additional cost to you.

OpenAI API

AI Platform

Access GPT-4 and other powerful AI models for your agent development.

Pay-per-use

LangChain Plus

Framework

Advanced framework for building applications with large language models.

Free + Paid

Pinecone Vector Database

Database

High-performance vector database for AI applications and semantic search.

Free tier available

AI Agent Development Course

Education

Complete course on building production-ready AI agents from scratch.

$199

πŸ’‘ Pro Tip

Start with the free tiers of these tools to experiment, then upgrade as your AI agent projects grow. Most successful developers use a combination of 2-3 core tools rather than trying everything at once.

πŸš€ Join the AgentForge Community

Get weekly insights, tutorials, and the latest AI agent developments delivered to your inbox.

No spam, ever. Unsubscribe at any time.

Loading conversations...