# RAG-Powered News Chatbot - Full Stack Implementation
## Overview
A complete full-stack chatbot application that answers queries over a news corpus
using Retrieval-Augmented Generation (RAG) pipeline with session management.
## Tech Stack Justification
- **Embeddings**: Sentence Transformers (all-MiniLM-L6-v2) - Free, fast, and
efficient
- **Vector DB**: Chroma - Simple setup, good for development
- **LLM API**: Google Gemini - As specified
- **Backend**: [Link] + Express - Fast development, good ecosystem
- **Cache & Sessions**: Redis - Industry standard for session management
- **Database**: PostgreSQL - Reliable, good JSON support for chat history
- **Frontend**: React + SCSS - Modern, responsive UI
## Project Structure
```
rag-news-chatbot/
├── backend/
│ ├── src/
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── models/
│ │ ├── routes/
│ │ └── utils/
│ ├── [Link]
│ └── .env
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── styles/
│ │ └── services/
│ ├── [Link]
│ └── public/
└── [Link]
```
## Backend Implementation
### 1. [Link] (Backend)
```json
{
"name": "rag-news-chatbot-backend",
"version": "1.0.0",
"description": "RAG-powered news chatbot backend",
"main": "src/[Link]",
"scripts": {
"start": "node src/[Link]",
"dev": "nodemon src/[Link]",
"setup-db": "node src/scripts/[Link]",
"ingest-news": "node src/scripts/[Link]"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"redis": "^4.6.7",
"pg": "^8.11.1",
"chromadb": "^1.5.13",
"@google/generative-ai": "^0.2.1",
"@xenova/transformers": "^2.10.0",
"axios": "^1.5.0",
"cheerio": "^1.0.0-rc.12",
"rss-parser": "^3.13.0",
"uuid": "^9.0.0",
"[Link]": "^4.7.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
```
### 2. Server Setup (src/[Link])
```javascript
const express = require('express');
const cors = require('cors');
const http = require('http');
const socketIo = require('[Link]');
require('dotenv').config();
const chatRoutes = require('./routes/chat');
const sessionRoutes = require('./routes/session');
const redisClient = require('./utils/redis');
const { initializeChroma } = require('./services/vectorStore');
const app = express();
const server = [Link](app);
const io = socketIo(server, {
cors: {
origin: [Link].FRONTEND_URL || "[Link]
methods: ["GET", "POST"]
}
});
// Middleware
[Link](cors());
[Link]([Link]());
// Routes
[Link]('/api/chat', chatRoutes);
[Link]('/api/session', sessionRoutes);
// [Link] for real-time chat
[Link]('connection', (socket) => {
[Link]('User connected:', [Link]);
[Link]('join-session', (sessionId) => {
[Link](sessionId);
});
[Link]('disconnect', () => {
[Link]('User disconnected:', [Link]);
});
});
// Initialize services
async function startServer() {
try {
await [Link]();
await initializeChroma();
const PORT = [Link] || 5000;
[Link](PORT, () => {
[Link](`Server running on port ${PORT}`);
});
} catch (error) {
[Link]('Failed to start server:', error);
[Link](1);
}
}
// Export io for use in routes
[Link]('io', io);
startServer();
```
### 3. Redis Configuration (src/utils/[Link])
```javascript
const redis = require('redis');
const client = [Link]({
host: [Link].REDIS_HOST || 'localhost',
port: [Link].REDIS_PORT || 6379,
// Configure TTL for sessions (24 hours)
database: 0
});
[Link]('error', (err) => {
[Link]('Redis Client Error:', err);
});
[Link]('connect', () => {
[Link]('Connected to Redis');
});
[Link] = client;
```
### 4. Vector Store Service (src/services/[Link])
```javascript
const { ChromaClient } = require('chromadb');
const { pipeline } = require('@xenova/transformers');
class VectorStore {
constructor() {
[Link] = new ChromaClient();
[Link] = null;
[Link] = null;
}
async initialize() {
try {
// Initialize embedding model
[Link] = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-
v2');
// Get or create collection
try {
[Link] = await [Link]({ name:
'news_articles' });
} catch (error) {
[Link] = await [Link]({
name: 'news_articles',
metadata: { description: 'News articles embeddings' }
});
}
[Link]('Vector store initialized');
} catch (error) {
[Link]('Failed to initialize vector store:', error);
throw error;
}
}
async addDocuments(documents) {
const embeddings = [];
const ids = [];
const metadatas = [];
const texts = [];
for (let i = 0; i < [Link]; i++) {
const doc = documents[i];
const embedding = await [Link]([Link], { pooling: 'mean',
normalize: true });
[Link]([Link]([Link]));
[Link]([Link] || `doc_${i}`);
[Link]({
title: [Link],
url: [Link],
publishedDate: [Link],
source: [Link]
});
[Link]([Link]);
}
await [Link]({
ids,
embeddings,
metadatas,
documents: texts
});
[Link](`Added ${[Link]} documents to vector store`);
}
async search(query, topK = 5) {
const queryEmbedding = await [Link](query, { pooling: 'mean', normalize:
true });
const results = await [Link]({
queryEmbeddings: [[Link]([Link])],
nResults: topK,
include: ['documents', 'metadatas', 'distances']
});
return [Link][0].map((doc, idx) => ({
content: doc,
metadata: [Link][0][idx],
score: [Link][0][idx]
}));
}
}
const vectorStore = new VectorStore();
async function initializeChroma() {
await [Link]();
}
[Link] = { vectorStore, initializeChroma };
```
### 5. LLM Service (src/services/[Link])
```javascript
const { GoogleGenerativeAI } = require('@google/generative-ai');
class LLMService {
constructor() {
[Link] = new GoogleGenerativeAI([Link].GEMINI_API_KEY);
[Link] = [Link]({ model: 'gemini-pro' });
}
async generateResponse(query, context, chatHistory = []) {
const contextText = [Link](doc =>
`Title: ${[Link]}\nContent: ${[Link]}\nSource: $
{[Link]}\n---`
).join('\n\n');
const historyText = [Link](-4).map(msg =>
`${[Link]}: ${[Link]}`
).join('\n');
const prompt = `You are a helpful news assistant. Answer the user's question
based on the provided news articles context.
If the answer cannot be found in the context, say so politely.
Context from news articles:
${contextText}
${historyText ? `Recent conversation history:\n${historyText}\n` : ''}
User question: ${query}
Please provide a helpful and accurate answer based on the news context:`;
try {
const result = await [Link](prompt);
const response = await [Link];
return [Link]();
} catch (error) {
[Link]('LLM Error:', error);
throw new Error('Failed to generate response');
}
}
}
[Link] = new LLMService();
```
### 6. Session Service (src/services/[Link])
```javascript
const redisClient = require('../utils/redis');
const { v4: uuidv4 } = require('uuid');
class SessionService {
constructor() {
[Link] = 24 * 60 * 60; // 24 hours in seconds
}
generateSessionId() {
return uuidv4();
}
async getSessionHistory(sessionId) {
try {
const history = await [Link](`session:${sessionId}`);
return history ? [Link](history) : [];
} catch (error) {
[Link]('Error getting session history:', error);
return [];
}
}
async addMessageToSession(sessionId, message) {
try {
const history = await [Link](sessionId);
[Link]({
...message,
timestamp: new Date().toISOString()
});
await [Link](
`session:${sessionId}`,
[Link],
[Link](history)
);
return history;
} catch (error) {
[Link]('Error adding message to session:', error);
throw error;
}
}
async clearSession(sessionId) {
try {
await [Link](`session:${sessionId}`);
return true;
} catch (error) {
[Link]('Error clearing session:', error);
return false;
}
}
async extendSessionTTL(sessionId) {
try {
await [Link](`session:${sessionId}`, [Link]);
} catch (error) {
[Link]('Error extending session TTL:', error);
}
}
}
[Link] = new SessionService();
```
### 7. Chat Routes (src/routes/[Link])
```javascript
const express = require('express');
const router = [Link]();
const { vectorStore } = require('../services/vectorStore');
const llmService = require('../services/llm');
const sessionService = require('../services/sessionService');
// Chat endpoint
[Link]('/', async (req, res) => {
try {
const { query, sessionId } = [Link];
if (!query || !sessionId) {
return [Link](400).json({ error: 'Query and sessionId are required' });
}
// Get chat history
const chatHistory = await [Link](sessionId);
// Add user message to session
await [Link](sessionId, {
role: 'user',
content: query
});
// Retrieve relevant documents
const relevantDocs = await [Link](query, 5);
// Generate response
const response = await [Link](query, relevantDocs,
chatHistory);
// Add bot response to session
await [Link](sessionId, {
role: 'assistant',
content: response,
sources: [Link](doc => ({
title: [Link],
url: [Link],
source: [Link]
}))
});
// Extend session TTL
await [Link](sessionId);
// Emit response via socket if available
const io = [Link]('io');
[Link](sessionId).emit('bot-response', {
response,
sources: [Link](doc => ({
title: [Link],
url: [Link],
source: [Link]
}))
});
[Link]({
response,
sources: [Link](doc => ({
title: [Link],
url: [Link],
source: [Link]
}))
});
} catch (error) {
[Link]('Chat error:', error);
[Link](500).json({ error: 'Internal server error' });
}
});
[Link] = router;
```
### 8. Session Routes (src/routes/[Link])
```javascript
const express = require('express');
const router = [Link]();
const sessionService = require('../services/sessionService');
// Create new session
[Link]('/create', (req, res) => {
const sessionId = [Link]();
[Link]({ sessionId });
});
// Get session history
[Link]('/:sessionId/history', async (req, res) => {
try {
const { sessionId } = [Link];
const history = await [Link](sessionId);
[Link]({ history });
} catch (error) {
[Link]('Error getting session history:', error);
[Link](500).json({ error: 'Internal server error' });
}
});
// Clear session
[Link]('/:sessionId', async (req, res) => {
try {
const { sessionId } = [Link];
const success = await [Link](sessionId);
[Link]({ success });
} catch (error) {
[Link]('Error clearing session:', error);
[Link](500).json({ error: 'Internal server error' });
}
});
[Link] = router;
```
### 9. News Ingestion Script (src/scripts/[Link])
```javascript
const RSSParser = require('rss-parser');
const axios = require('axios');
const cheerio = require('cheerio');
const { vectorStore } = require('../services/vectorStore');
class NewsIngestion {
constructor() {
[Link] = new RSSParser();
[Link] = [
'[Link]
'[Link]
'[Link]
'[Link]
// Add more RSS feeds as needed
];
}
async fetchRSSFeed(url) {
try {
const feed = await [Link](url);
return [Link](0, 15); // Limit per source
} catch (error) {
[Link](`Error fetching RSS from ${url}:`, [Link]);
return [];
}
}
async extractArticleContent(url) {
try {
const response = await [Link](url, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36'
}
});
const $ = [Link]([Link]);
// Remove unnecessary elements
$('script, style, nav, header, footer, aside, .advertisement').remove();
// Extract main content (adapt selectors as needed)
let content = $('article, main, .content, .story-body, .entry-
content').text();
if (!content) {
content = $('p').text();
}
return [Link]().substring(0, 2000); // Limit content length
} catch (error) {
[Link](`Error extracting content from ${url}:`, [Link]);
return '';
}
}
async ingestNews() {
[Link]('Starting news ingestion...');
const allArticles = [];
for (const sourceUrl of [Link]) {
[Link](`Fetching from ${sourceUrl}...`);
const articles = await [Link](sourceUrl);
for (const article of articles) {
const content = await [Link]([Link]);
if (content && [Link] > 100) {
[Link]({
id: [Link]([Link]).toString('base64'),
title: [Link],
content: content,
url: [Link],
publishedDate: [Link],
source: new URL(sourceUrl).hostname
});
}
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
[Link](`Ingested ${[Link]} articles`);
// Add to vector store
if ([Link] > 0) {
await [Link](allArticles);
[Link]('Articles added to vector store');
}
return [Link];
}
}
// Run ingestion if called directly
if ([Link] === module) {
const ingestion = new NewsIngestion();
[Link]().then(count => {
[Link](`Successfully ingested ${count} articles`);
[Link](0);
}).catch(error => {
[Link]('Ingestion failed:', error);
[Link](1);
});
}
[Link] = NewsIngestion;
```
## Frontend Implementation
### 1. [Link] (Frontend)
```json
{
"name": "rag-news-chatbot-frontend",
"version": "1.0.0",
"description": "RAG-powered news chatbot frontend",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.5.0",
"[Link]-client": "^4.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"devDependencies": {
"react-scripts": "^5.0.1",
"sass": "^1.66.1"
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": ["last 1 chrome version", "last 1 firefox version", "last 1
safari version"]
}
}
```
### 2. Main App Component (src/[Link])
```jsx
import React, { useState, useEffect } from 'react';
import ChatInterface from './components/ChatInterface';
import { createSession } from './services/api';
import './styles/[Link]';
function App() {
const [sessionId, setSessionId] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
initializeSession();
}, []);
const initializeSession = async () => {
try {
const response = await createSession();
setSessionId([Link]);
} catch (error) {
[Link]('Failed to create session:', error);
} finally {
setLoading(false);
}
};
const handleNewSession = async () => {
setLoading(true);
await initializeSession();
};
if (loading) {
return (
<div className="app-loading">
<div className="loading-spinner"></div>
<p>Initializing chatbot...</p>
</div>
);
}
return (
<div className="app">
<header className="app-header">
<div className="header-content">
<h1>📰 News AI Assistant</h1>
<p>Ask questions about recent news and get AI-powered answers</p>
<button
className="new-session-btn"
onClick={handleNewSession}
>
🔄 New Session
</button>
</div>
</header>
<main className="app-main">
{sessionId && (
<ChatInterface
sessionId={sessionId}
onNewSession={handleNewSession}
/>
)}
</main>
</div>
);
}
export default App;
```
### 3. Chat Interface Component (src/components/[Link])
```jsx
import React, { useState, useEffect, useRef } from 'react';
import { sendMessage, getSessionHistory, clearSession } from '../services/api';
import { connectSocket, disconnectSocket } from '../services/socket';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import TypingIndicator from './TypingIndicator';
import '../styles/[Link]';
const ChatInterface = ({ sessionId, onNewSession }) => {
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const chatContainerRef = useRef(null);
useEffect(() => {
if (sessionId) {
loadSessionHistory();
setupSocket();
}
return () => {
disconnectSocket();
};
}, [sessionId]);
useEffect(() => {
scrollToBottom();
}, [messages]);
const loadSessionHistory = async () => {
try {
const response = await getSessionHistory(sessionId);
setMessages([Link] || []);
} catch (error) {
[Link]('Failed to load session history:', error);
}
};
const setupSocket = () => {
connectSocket(sessionId, (response) => {
setIsTyping(false);
// Socket response is handled by the API response
});
};
const scrollToBottom = () => {
if ([Link]) {
[Link] = [Link];
}
};
const handleSendMessage = async (messageText) => {
if (![Link]()) return;
const userMessage = {
role: 'user',
content: messageText,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, userMessage]);
setIsLoading(true);
setIsTyping(true);
try {
const response = await sendMessage(messageText, sessionId);
const botMessage = {
role: 'assistant',
content: [Link],
sources: [Link],
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, botMessage]);
} catch (error) {
[Link]('Failed to send message:', error);
const errorMessage = {
role: 'assistant',
content: 'Sorry, I encountered an error processing your request. Please try
again.',
timestamp: new Date().toISOString(),
isError: true
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
setIsTyping(false);
}
};
const handleClearSession = async () => {
if ([Link]('Are you sure you want to clear this session?')) {
try {
await clearSession(sessionId);
setMessages([]);
} catch (error) {
[Link]('Failed to clear session:', error);
}
}
};
return (
<div className="chat-interface">
<div className="chat-header">
<div className="session-info">
<span className="session-id">Session: {[Link](0,
8)}...</span>
<div className="session-actions">
<button
className="clear-btn"
onClick={handleClearSession}
title="Clear chat history"
>
Clear
</button>
</div>
</div>
</div>
<div className="chat-container" ref={chatContainerRef}>
<MessageList messages={messages} />
{isTyping && <TypingIndicator />}
</div>
<MessageInput
onSendMessage={handleSendMessage}
disabled={isLoading}
/>
</div>
);
};
export default ChatInterface;
```
### 4. Message List Component (src/components/[Link])
```jsx
import React from 'react';
import MessageBubble from './MessageBubble';
import '../styles/[Link]';
const MessageList = ({ messages }) => {
if ([Link] === 0) {
return (
<div className="message-list empty">
<div className="welcome-message">
<h3>👋 Welcome to News AI Assistant!</h3>
<p>Ask me anything about recent news and current events.</p>
<div className="example-questions">
<p>Try asking:</p>
<ul>
<li>"What's the latest news about technology?"</li>
<li>"Tell me about recent political developments"</li>
<li>"What are the top business stories today?"</li>
</ul>
</div>
</div>
</div>
);
}
return (
<div className="message-list">
{[Link]((message, index) => (
<MessageBubble
key={`${[Link]}-${index}`}
message={message}
/>
))}
</div>
);
};
export default MessageList;
```
### 5. Message Bubble Component (src/components/[Link])
```jsx
import React, { useState, useEffect } from 'react';
import '../styles/[Link]';
const MessageBubble = ({ message }) => {
const [displayedContent, setDisplayedContent] = useState('');
const [isComplete, setIsComplete] = useState(false);
useEffect(() => {
if ([Link] === 'assistant' && ![Link]) {
// Simulate typing effect for bot messages
let index = 0;
const content = [Link];
const typeWriter = () => {
if (index < [Link]) {
setDisplayedContent([Link](0, index + 1));
index++;
setTimeout(typeWriter, 20);
} else {
setIsComplete(true);
}
};
typeWriter();
} else {
setDisplayedContent([Link]);
setIsComplete(true);
}
}, [message]);
const formatTimestamp = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<div className={`message-bubble ${[Link]}`}>
<div className="message-avatar">
{[Link] === 'user' ? '👤' : '🤖'}
</div>
<div className="message-content">
<div className={`message-text ${[Link] ? 'error' : ''}`}>
{displayedContent}
{!isComplete && [Link] === 'assistant' && (
<span className="typing-cursor">|</span>
)}
</div>
{[Link] && [Link] > 0 && isComplete && (
<div className="message-sources">
<h4>📚 Sources:</h4>
<ul>
{[Link]((source, index) => (
<li key={index}>
<a href={[Link]} target="_blank" rel="noopener noreferrer">
{[Link]}
</a>
<span className="source-name">- {[Link]}</span>
</li>
))}
</ul>
</div>
)}
<div className="message-timestamp">
{formatTimestamp([Link])}
</div>
</div>
</div>
);
};
export default MessageBubble;
```
### 6. Message Input Component (src/components/[Link])
```jsx
import React, { useState } from 'react';
import '../styles/[Link]';
const MessageInput = ({ onSendMessage, disabled }) => {
const [message, setMessage] = useState('');
const handleSubmit = (e) => {
[Link]();
if ([Link]() && !disabled) {
onSendMessage(message);
setMessage('');
}
};
const handleKeyPress = (e) => {
if ([Link] === 'Enter' && ![Link]) {
[Link]();
handleSubmit(e);
}
};
return (
<form className="message-input" onSubmit={handleSubmit}>
<div className="input-container">
<textarea
value={message}
onChange={(e) => setMessage([Link])}
onKeyPress={handleKeyPress}
placeholder="Ask me about recent news..."
disabled={disabled}
rows="1"
/>
<button
type="submit"
disabled={disabled || ![Link]()}
className="send-button"
>
{disabled ? '⏳' : '📤'}
</button>
</div>
</form>
);
};
export default MessageInput;
```
### 7. Typing Indicator Component (src/components/[Link])
```jsx
import React from 'react';
import '../styles/[Link]';
const TypingIndicator = ()