DEV Community

Cover image for Smartchat AI Assistant with Plugin System
ADESH MISHRA
ADESH MISHRA

Posted on

Smartchat AI Assistant with Plugin System

Building and Deploying a Smart AI Chat Assistant with Plugin System

Introduction

In today's digital landscape, conversational interfaces are becoming the norm. From customer support to personal assistants, chat-based applications are transforming how users interact with technology. In this comprehensive guide, I'll walk you through building a Smart AI Chat Assistant with an extensible plugin system that can understand natural language and perform various tasks.

What We're Building

Our chat assistant will feature:

🎯 Intelligent Plugin System - Extensible architecture for adding new capabilities

🌤️ Weather Integration - Real-time weather information for any city

🧮 Calculator Functionality - Mathematical expression evaluation

📖 Dictionary Lookup - Word definitions and meanings

💬 Natural Language Processing - Understands conversational queries

🗄️ Persistent Chat History - MongoDB-powered message storage

☁️ Cloud Deployment - Production-ready deployment on Render.com

Why This Architecture?

Before diving into the code, let's understand why this plugin-based approach is powerful:

Traditional Monolithic Approach

User Input → Single Handler → Hard-coded Responses
Enter fullscreen mode Exit fullscreen mode

Our Plugin-Based Approach

User Input → Intent Detection → Plugin Router → Specific Handler → Rich Response
Enter fullscreen mode Exit fullscreen mode

This architecture provides:

  • Scalability: Easy to add new features without touching core code
  • Maintainability: Each plugin is self-contained
  • Team Collaboration: Different developers can work on different plugins
  • Testing: Isolated testing of individual components

Project Architecture Deep Dive

ai-chat-interface/
├── client/                  # Frontend (React + Vite)
│   ├── public/
│   ├── src/
│   │   ├── components/
│   │   │   ├── Chat.jsx
│   │   │   ├── Message.jsx
│   │   │   ├── PluginCard.jsx
│   │   │   └── ...
│   │   ├── context/
│   │   │   └── ChatContext.js
│   │   ├── App.jsx
│   │   ├── main.jsx
│   │   └── ...
│   ├── package.json
│   └── ...
├── server/                  # Backend (Node.js + Express)
│   ├── controllers/
│   │   ├── chatController.js
│   │   ├── weatherController.js
│   │   ├── calculatorController.js
│   │   └── dictionaryController.js
│   ├── models/
│   │   ├── messageModel.js
│   │   └── ...
│   ├── routes/
│   │   ├── chatRoutes.js
│   │   ├── weatherRoutes.js
│   │   ├── calculatorRoutes.js
│   │   └── dictionaryRoutes.js
│   ├── app.js
│   ├── server.js
│   ├── package.json
│   └── ...
└── README.md
Enter fullscreen mode Exit fullscreen mode

Building the Backend: Plugin Architecture

1. Setting Up the Foundation

First, let's create our Express server with proper middleware configuration:

// server/app.js
import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import dotenv from 'dotenv';

dotenv.config();

const app = express();

// Middleware
app.use(cors({
  origin: process.env.NODE_ENV === 'production' 
    ? ['https://your-frontend-domain.com'] 
    : ['http://localhost:5173'],
  credentials: true
}));

app.use(express.json());
app.use(express.static('public'));

// Database Connection
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// Plugin Routes
app.use('/api/weather', require('./routes/weather'));
app.use('/api/calc', require('./routes/calculator'));
app.use('/api/define', require('./routes/dictionary'));

const PORT = process.env.PORT || 10000;
app.listen(PORT, () => {
  console.log(`🚀 Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

2. Creating the Weather Plugin

The weather plugin demonstrates how external API integration works within our architecture:

// server/controllers/weatherController.js
import axios from 'axios';

export const getWeather = async (req, res) => {
  try {
    const { city } = req.params;

    // Input validation
    if (!city || city.trim().length === 0) {
      return res.status(400).json({ 
        success: false, 
        error: 'City name is required' 
      });
    }

    const response = await axios.get(
      `https://api.openweathermap.org/data/2.5/weather`, {
        params: {
          q: city,
          appid: process.env.WEATHER_API_KEY,
          units: 'metric'
        }
      }
    );

    const weatherData = {
      city: response.data.name,
      country: response.data.sys.country,
      temperature: Math.round(response.data.main.temp),
      description: response.data.weather[0].description,
      icon: response.data.weather[0].icon,
      humidity: response.data.main.humidity,
      windSpeed: response.data.wind.speed,
      timestamp: new Date().toISOString()
    };

    res.json({
      success: true,
      data: weatherData,
      plugin: 'weather'
    });

  } catch (error) {
    console.error('Weather API Error:', error.message);

    if (error.response?.status === 404) {
      return res.status(404).json({ 
        success: false, 
        error: 'City not found. Please check the spelling and try again.' 
      });
    }

    res.status(500).json({ 
      success: false, 
      error: 'Unable to fetch weather data. Please try again later.' 
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Building the Calculator Plugin

The calculator plugin showcases secure expression evaluation:

// server/controllers/calcController.js
import { evaluate } from 'mathjs';

export const calculateExpression = async (req, res) => {
  try {
    const { expression } = req.params;

    // Security: Validate expression
    const allowedChars = /^[0-9+\-*/().\s]+$/;
    if (!allowedChars.test(expression)) {
      return res.status(400).json({ 
        success: false, 
        error: 'Invalid characters in expression' 
      });
    }

    // Evaluate the expression safely
    const result = evaluate(expression);

    res.json({
      success: true,
      data: {
        expression: expression,
        result: result,
        formatted: `${expression} = ${result}`
      },
      plugin: 'calculator'
    });

  } catch (error) {
    res.status(400).json({ 
      success: false, 
      error: 'Invalid mathematical expression' 
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

4. Dictionary Plugin Implementation

// server/controllers/dictionaryController.js
import axios from 'axios';

export const getDefinition = async (req, res) => {
  try {
    const { word } = req.params;

    const response = await axios.get(
      `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`
    );

    const wordData = response.data[0];
    const definition = {
      word: wordData.word,
      phonetic: wordData.phonetic || '',
      meanings: wordData.meanings.map(meaning => ({
        partOfSpeech: meaning.partOfSpeech,
        definition: meaning.definitions[0].definition,
        example: meaning.definitions[0].example || ''
      }))
    };

    res.json({
      success: true,
      data: definition,
      plugin: 'dictionary'
    });

  } catch (error) {
    res.status(404).json({ 
      success: false, 
      error: 'Word not found in dictionary' 
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Frontend Development: React Components

1. Chat Context for State Management

// frontend/src/context/ChatContext.jsx
import React, { createContext, useContext, useReducer } from 'react';
import { sendMessage, fetchPluginData } from '../utils/api';

const ChatContext = createContext();

const initialState = {
  messages: [],
  loading: false,
  error: null
};

const chatReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_MESSAGE':
      return {
        ...state,
        messages: [...state.messages, action.payload]
      };
    case 'SET_LOADING':
      return {
        ...state,
        loading: action.payload
      };
    case 'SET_ERROR':
      return {
        ...state,
        error: action.payload
      };
    default:
      return state;
  }
};

export const ChatProvider = ({ children }) => {
  const [state, dispatch] = useReducer(chatReducer, initialState);

  const processMessage = async (message) => {
    // Add user message
    dispatch({
      type: 'ADD_MESSAGE',
      payload: {
        id: Date.now(),
        text: message,
        sender: 'user',
        timestamp: new Date()
      }
    });

    dispatch({ type: 'SET_LOADING', payload: true });

    try {
      // Detect intent and route to appropriate plugin
      const response = await detectIntentAndProcess(message);

      dispatch({
        type: 'ADD_MESSAGE',
        payload: {
          id: Date.now() + 1,
          ...response,
          sender: 'assistant',
          timestamp: new Date()
        }
      });
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message });
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };

  const detectIntentAndProcess = async (message) => {
    const lowerMessage = message.toLowerCase();

    // Weather intent detection
    if (lowerMessage.includes('weather') || lowerMessage.startsWith('/weather')) {
      const cityMatch = message.match(/(?:weather|\/weather)\s+(?:in\s+)?([a-zA-Z\s]+)/i);
      const city = cityMatch ? cityMatch[1].trim() : 'London';

      const data = await fetchPluginData('weather', city);
      return {
        type: 'plugin',
        plugin: 'weather',
        data: data
      };
    }

    // Calculator intent detection
    if (lowerMessage.includes('calculate') || lowerMessage.startsWith('/calc')) {
      const expressionMatch = message.match(/(?:calculate|\/calc)\s+(.+)/i);
      const expression = expressionMatch ? expressionMatch[1].trim() : '2+2';

      const data = await fetchPluginData('calc', expression);
      return {
        type: 'plugin',
        plugin: 'calculator',
        data: data
      };
    }

    // Dictionary intent detection
    if (lowerMessage.includes('define') || lowerMessage.startsWith('/define')) {
      const wordMatch = message.match(/(?:define|\/define)\s+([a-zA-Z]+)/i);
      const word = wordMatch ? wordMatch[1].trim() : 'hello';

      const data = await fetchPluginData('define', word);
      return {
        type: 'plugin',
        plugin: 'dictionary',
        data: data
      };
    }

    // Default response
    return {
      type: 'text',
      text: 'I can help you with weather information, calculations, and word definitions. Try saying "What\'s the weather in Paris?" or "/calc 2+2"'
    };
  };

  return (
    <ChatContext.Provider value={{
      ...state,
      processMessage
    }}>
      {children}
    </ChatContext.Provider>
  );
};

export const useChat = () => {
  const context = useContext(ChatContext);
  if (!context) {
    throw new Error('useChat must be used within a ChatProvider');
  }
  return context;
};
Enter fullscreen mode Exit fullscreen mode

2. Plugin Card Components

// frontend/src/components/PluginCard.jsx
import React from 'react';

const PluginCard = ({ plugin, data }) => {
  const renderWeatherCard = () => (
    <div className="bg-gradient-to-br from-blue-400 to-blue-600 text-white rounded-xl p-6 shadow-lg max-w-sm">
      <div className="flex items-center justify-between mb-4">
        <div>
          <h3 className="text-xl font-bold">{data.city}, {data.country}</h3>
          <p className="text-blue-100">{data.description}</p>
        </div>
        <div className="text-3xl">
          <img 
            src={`https://openweathermap.org/img/w/${data.icon}.png`} 
            alt={data.description}
            className="w-16 h-16"
          />
        </div>
      </div>

      <div className="grid grid-cols-2 gap-4 text-sm">
        <div>
          <p className="text-blue-200">Temperature</p>
          <p className="text-2xl font-bold">{data.temperature}°C</p>
        </div>
        <div>
          <p className="text-blue-200">Humidity</p>
          <p className="text-lg font-semibold">{data.humidity}%</p>
        </div>
      </div>
    </div>
  );

  const renderCalculatorCard = () => (
    <div className="bg-gradient-to-br from-green-400 to-green-600 text-white rounded-xl p-6 shadow-lg max-w-sm">
      <div className="flex items-center gap-3 mb-4">
        <div className="text-2xl">🧮</div>
        <h3 className="text-xl font-bold">Calculator</h3>
      </div>

      <div className="bg-white bg-opacity-20 rounded-lg p-4">
        <p className="text-green-100 text-sm mb-2">Expression:</p>
        <p className="font-mono text-lg">{data.expression}</p>

        <p className="text-green-100 text-sm mt-4 mb-2">Result:</p>
        <p className="font-bold text-2xl">{data.result}</p>
      </div>
    </div>
  );

  const renderDictionaryCard = () => (
    <div className="bg-gradient-to-br from-purple-400 to-purple-600 text-white rounded-xl p-6 shadow-lg max-w-md">
      <div className="flex items-center gap-3 mb-4">
        <div className="text-2xl">📖</div>
        <div>
          <h3 className="text-xl font-bold">{data.word}</h3>
          {data.phonetic && <p className="text-purple-200 text-sm">{data.phonetic}</p>}
        </div>
      </div>

      <div className="space-y-3">
        {data.meanings.map((meaning, index) => (
          <div key={index} className="bg-white bg-opacity-20 rounded-lg p-3">
            <p className="text-purple-200 text-sm font-semibold">{meaning.partOfSpeech}</p>
            <p className="text-sm mt-1">{meaning.definition}</p>
            {meaning.example && (
              <p className="text-purple-200 text-xs mt-2 italic">"{meaning.example}"</p>
            )}
          </div>
        ))}
      </div>
    </div>
  );

  switch (plugin) {
    case 'weather':
      return renderWeatherCard();
    case 'calculator':
      return renderCalculatorCard();
    case 'dictionary':
      return renderDictionaryCard();
    default:
      return (
        <div className="bg-gray-100 rounded-lg p-4 shadow-md">
          <p>Unknown plugin: {plugin}</p>
        </div>
      );
  }
};

export default PluginCard;
Enter fullscreen mode Exit fullscreen mode

Deployment Strategy: Production-Ready Setup

1. Environment Configuration

Backend Environment Variables:

# Database
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/smartchat

# External APIs
WEATHER_API_KEY=your_openweathermap_api_key

# Server Configuration
PORT=10000
NODE_ENV=production

# Security
JWT_SECRET=your_jwt_secret_key
CORS_ORIGIN=https://your-frontend-domain.com
Enter fullscreen mode Exit fullscreen mode

Frontend Environment Variables:

# API Configuration
VITE_API_URL=https://your-backend-service.onrender.com
VITE_APP_NAME=SmartChat AI Assistant
Enter fullscreen mode Exit fullscreen mode

2. Render.com Deployment Configuration

Backend Service Configuration:

# render.yaml
services:
  - type: web
    name: smartchat-backend
    env: node
    buildCommand: npm install && npm run build
    startCommand: node server.js
    envVars:
      - key: MONGODB_URI
        sync: false
      - key: WEATHER_API_KEY
        sync: false
      - key: NODE_ENV
        value: production
Enter fullscreen mode Exit fullscreen mode

Frontend Static Site Configuration:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: false,
    minify: 'esbuild'
  },
  server: {
    proxy: {
      '/api': {
        target: process.env.VITE_API_URL || 'http://localhost:10000',
        changeOrigin: true,
        secure: true
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

3. MongoDB Atlas Setup

  1. Create Cluster: Set up a free MongoDB Atlas cluster
  2. Network Access: Add 0.0.0.0/0 for Render deployment
  3. Database User: Create user with read/write permissions
  4. Connection String: Get connection string for your application

Advanced Features & Optimizations

1. Caching Strategy

// server/utils/cache.js
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes

export const getCachedData = (key) => {
  return cache.get(key);
};

export const setCachedData = (key, data) => {
  return cache.set(key, data);
};

// Usage in weather controller
export const getWeather = async (req, res) => {
  const { city } = req.params;
  const cacheKey = `weather_${city.toLowerCase()}`;

  // Check cache first
  const cachedData = getCachedData(cacheKey);
  if (cachedData) {
    return res.json(cachedData);
  }

  // Fetch and cache new data
  const weatherData = await fetchWeatherData(city);
  setCachedData(cacheKey, weatherData);

  res.json(weatherData);
};
Enter fullscreen mode Exit fullscreen mode

2. Rate Limiting

// server/middleware/rateLimit.js
import rateLimit from 'express-rate-limit';

export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests, please try again later.'
  }
});

export const pluginLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 10, // limit each IP to 10 plugin requests per minute
  message: {
    error: 'Plugin rate limit exceeded. Please wait before making another request.'
  }
});
Enter fullscreen mode Exit fullscreen mode

3. Error Handling Middleware

// server/middleware/errorHandler.js
export const errorHandler = (err, req, res, next) => {
  console.error(err.stack);

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      success: false,
      error: 'Validation Error',
      details: Object.values(err.errors).map(e => e.message)
    });
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({
      success: false,
      error: 'Invalid token'
    });
  }

  // Default error
  res.status(500).json({
    success: false,
    error: 'Internal server error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
  });
};
Enter fullscreen mode Exit fullscreen mode

Performance Monitoring & Analytics

1. Logging System

// server/utils/logger.js
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

export default logger;
Enter fullscreen mode Exit fullscreen mode

2. Usage Analytics

// server/models/Analytics.js
import mongoose from 'mongoose';

const analyticsSchema = new mongoose.Schema({
  plugin: {
    type: String,
    required: true
  },
  query: String,
  responseTime: Number,
  success: Boolean,
  userAgent: String,
  timestamp: {
    type: Date,
    default: Date.now
  }
});

export default mongoose.model('Analytics', analyticsSchema);
Enter fullscreen mode Exit fullscreen mode

Testing Strategy

1. Backend Unit Tests

// server/tests/weather.test.js
import request from 'supertest';
import app from '../app.js';

describe('Weather Plugin', () => {
  test('should return weather data for valid city', async () => {
    const response = await request(app)
      .get('/api/weather/London')
      .expect(200);

    expect(response.body.success).toBe(true);
    expect(response.body.data.city).toBe('London');
    expect(response.body.data.temperature).toBeDefined();
  });

  test('should return error for invalid city', async () => {
    const response = await request(app)
      .get('/api/weather/InvalidCityName123')
      .expect(404);

    expect(response.body.success).toBe(false);
    expect(response.body.error).toContain('not found');
  });
});
Enter fullscreen mode Exit fullscreen mode

2. Frontend Component Tests

// frontend/src/components/__tests__/PluginCard.test.jsx
import { render, screen } from '@testing-library/react';
import PluginCard from '../PluginCard';

const mockWeatherData = {
  city: 'London',
  country: 'GB',
  temperature: 20,
  description: 'Clear sky',
  icon: '01d'
};

test('renders weather card correctly', () => {
  render(<PluginCard plugin="weather" data={mockWeatherData} />);

  expect(screen.getByText('London, GB')).toBeInTheDocument();
  expect(screen.getByText('20°C')).toBeInTheDocument();
  expect(screen.getByText('Clear sky')).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

1. Input Validation & Sanitization

// server/middleware/validation.js
import { body, param, validationResult } from 'express-validator';

export const validateCity = [
  param('city')
    .isLength({ min: 1, max: 50 })
    .matches(/^[a-zA-Z\s\-']+$/)
    .withMessage('City name contains invalid characters'),

  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        success: false,
        error: 'Validation failed',
        details: errors.array()
      });
    }
    next();
  }
];
Enter fullscreen mode Exit fullscreen mode

2. Environment Security

// server/config/security.js
import helmet from 'helmet';
import mongoSanitize from 'express-mongo-sanitize';
import xss from 'xss-clean';

export const securityMiddleware = [
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"],
      },
    },
  }),
  mongoSanitize(),
  xss()
];
Enter fullscreen mode Exit fullscreen mode

Future Enhancements & Roadmap

Phase 1: Core Improvements

  • User Authentication: JWT-based user sessions
  • Chat Persistence: Save conversations per user
  • Plugin Marketplace: Community-contributed plugins
  • Voice Interface: Speech-to-text integration

Phase 2: Advanced Features

  • AI Integration: OpenAI GPT integration for natural responses
  • Multi-language Support: Internationalization
  • Real-time Collaboration: WebSocket-based live chat
  • Mobile App: React Native companion app

Phase 3: Enterprise Features

  • Analytics Dashboard: Usage statistics and insights
  • Admin Panel: Plugin management interface
  • API Gateway: Rate limiting and monitoring
  • Microservices: Split plugins into separate services

Lessons Learned & Best Practices

1. Architecture Decisions

✅ What Worked Well:

  • Plugin architecture made adding new features trivial
  • Separation of concerns improved code maintainability
  • Context API simplified state management
  • Environment-based configuration enabled smooth deployments

❌ Challenges Faced:

  • CORS configuration took time to get right
  • Error handling across plugins needed standardization
  • Rate limiting was essential for external API usage
  • Caching strategy improved performance significantly

2. Performance Insights

// Performance monitoring
const performanceMiddleware = (req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info(`${req.method} ${req.path} - ${duration}ms`);

    // Track slow requests
    if (duration > 1000) {
      logger.warn(`Slow request: ${req.method} ${req.path} - ${duration}ms`);
    }
  });

  next();
};
Enter fullscreen mode Exit fullscreen mode

3. User Experience Improvements

  • Loading States: Always show loading indicators
  • Error Messages: Provide helpful, actionable error messages
  • Responsive Design: Ensure mobile-first approach
  • Accessibility: Proper ARIA labels and keyboard navigation

Conclusion

Building this Smart AI Chat Assistant has been an incredible journey through modern web development practices. The plugin architecture we've implemented provides a solid foundation that can scale from a simple chat interface to a comprehensive AI platform.

Key Takeaways

  • Modular Architecture: Plugin-based systems are incredibly powerful for building scalable applications
  • User Experience: Natural language processing makes interfaces more intuitive
  • Cloud Deployment: Modern platforms like Render.com simplify deployment significantly
  • Security First: Always implement proper validation, sanitization, and rate limiting
  • Performance Matters: Caching and optimization are crucial for production applications

Next Steps

I encourage you to:

  1. Clone and Experiment: Try building your own plugins
  2. Contribute: Add new features or improve existing ones
  3. Deploy: Get hands-on experience with cloud deployment
  4. Scale: Think about how you'd handle thousands of users

Resources

Have you built something similar? I'd love to hear about your experience and any improvements you'd suggest. Feel free to reach out on Twitter or LinkedIn!

Top comments (0)