When I began my final year project to create a retrieval-augmented financial analysis chatbot for Malaysian markets, I needed a technology stack that could handle both the sophisticated backend processing and deliver a smooth, professional user experience. My solution combined Python with Flask on the backend and leveraged modern CSS and JavaScript techniques for the frontend.
Backend Architecture with Python and Flask
Why Python?
Python was the natural choice for the backend given the data processing and machine learning requirements. Its rich ecosystem of libraries made it ideal for:
- Document processing with PyPDF2 and Tesseract OCR
- Vector embeddings with the multilingual E5-large model
- Integration with the SEA-LION language model
- Natural language processing with spaCy
Flask: Lightweight but Powerful
While Django might seem like the obvious choice for a complex web application, I opted for Flask for its flexibility and lightweight nature. This proved to be the right decision as I needed to:
- Create custom RESTful API endpoints for the chatbot interactions
- Implement WebSocket connections for streaming responses
- Build specialized routing for different data processing pipelines
- Maintain efficient request handling for potentially long-running operations
Here’s a simplified example of my Flask application structure:
from flask import Flask, request, jsonify
from flask_socketio import SocketIO
import pinecone
from models import SEALionModel
from retrieval import DocumentRetriever
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
# Initialize components
pinecone.init(api_key=os.environ["PINECONE_API_KEY"], environment="asia-southeast1-gcp")
index = pinecone.Index("malaysia-finance")
model = SEALionModel()
retriever = DocumentRetriever(index)
@app.route('/api/query', methods=['POST'])
def process_query():
data = request.json
query = data.get('query', '')
user_id = data.get('user_id', 'anonymous')
# Process query
relevant_docs = retriever.retrieve(query)
response = model.generate_response(query, relevant_docs)
return jsonify({
'response': response,
'sources': [doc.metadata for doc in relevant_docs]
})
@socketio.on('stream_query')
def handle_stream_query(data):
query = data.get('query', '')
user_id = data.get('user_id', 'anonymous')
# Process and stream response
relevant_docs = retriever.retrieve(query)
for chunk in model.stream_response(query, relevant_docs):
socketio.emit('response_chunk', {'chunk': chunk}, room=request.sid)
# Send sources at the end
socketio.emit('response_complete', {
'sources': [doc.metadata for doc in relevant_docs]
}, room=request.sid)
if __name__ == '__main__':
socketio.run(app, debug=True)
Frontend Implementation
Modern CSS Techniques
For the user interface, I needed a design that was both professional and responsive. I used several modern CSS techniques:
- CSS Grid for the overall layout structure
- Flexbox for component alignment
- CSS Variables for consistent theming
- Media queries for responsive design across devices
One particularly effective pattern was using CSS Grid areas to create a conversation layout:
.chat-container {
display: grid;
grid-template-areas:
"header header"
"messages sidebar"
"input input";
grid-template-columns: 1fr 300px;
grid-template-rows: auto 1fr auto;
height: 100vh;
}
.chat-header { grid-area: header; }
.messages { grid-area: messages; }
.sidebar { grid-area: sidebar; }
.input-area { grid-area: input; }
@media (max-width: 768px) {
.chat-container {
grid-template-areas:
"header"
"messages"
"input";
grid-template-columns: 1fr;
}
.sidebar {
display: none; /* Hide on mobile, add toggle button */
}
}
Interactive User Experience with JavaScript
To create a responsive chatbot experience, I implemented several JavaScript features:
- Real-time response streaming using WebSockets
- Markdown rendering for formatted financial information
- Citation highlighting and source linking
- Syntax highlighting for code blocks
A simplified example of the WebSocket implementation:
// Establish WebSocket connection
const socket = io(API_ENDPOINT);
// Handle form submission
document.querySelector('#query-form').addEventListener('submit', function(e) {
e.preventDefault();
const query = document.querySelector('#query-input').value;
// Add user message to UI
addMessageToChat('user', query);
// Show typing indicator
showTypingIndicator();
// Send query through WebSocket
socket.emit('stream_query', {
query: query,
user_id: currentUser.id
});
// Clear input field
document.querySelector('#query-input').value = '';
});
// Initialize response container
let currentResponseContainer = null;
// Handle incoming response chunks
socket.on('response_chunk', function(data) {
if (!currentResponseContainer) {
hideTypingIndicator();
currentResponseContainer = addMessageToChat('assistant', '');
}
// Append new content and render markdown
currentResponseContainer.textContent += data.chunk;
renderMarkdown(currentResponseContainer);
});
// Handle response completion
socket.on('response_complete', function(data) {
// Add source citations
if (data.sources && data.sources.length > 0) {
const sourcesContainer = document.createElement('div');
sourcesContainer.className = 'sources';
const sourcesList = document.createElement('ul');
data.sources.forEach(source => {
const sourceItem = document.createElement('li');
sourceItem.innerHTML = `<a href="#" data-source-id="${source.id}">${source.title} (${source.year})</a>`;
sourcesList.appendChild(sourceItem);
});
sourcesContainer.appendChild(sourcesList);
currentResponseContainer.appendChild(sourcesContainer);
}
currentResponseContainer = null;
});
Integration Challenges and Solutions
One of the biggest challenges was managing the interaction between the Flask backend and the frontend, particularly for long-running operations like document retrieval and LLM inference. I addressed this through:
- Implementing proper request timeout handling
- Using WebSockets for streaming responses
- Adding background task processing with Celery for document updates
- Implementing client-side error recovery and retry logic
Lessons Learned
If I were to rebuild this project, I would make a few changes:
- Consider using FastAPI instead of Flask for better async support and automatic documentation
- Implement TypeScript for frontend development to catch type errors early
- Create a more modular CSS architecture using a methodology like BEM or SMACSS
- Add more comprehensive client-side state management, possibly with Redux
Overall, the combination of Python, Flask, and modern frontend technologies provided a solid foundation for building a sophisticated financial chatbot. The flexibility of this stack allowed me to focus on the unique challenges of multilingual financial analysis rather than fighting with the technology.
Leave a Reply