Building a Non-Generative AI Support Chatbot with Flask
In customer support, fast and reliable answers matter. I wanted to create a controlled, efficient support chatbot — one that doesn't rely on generative AI but instead uses traditional machine learning, information retrieval, and a custom-designed user interface for an interactive, predictible experience. In this project, I built a Flask web application that uses SVM classification, TF-IDF vectorisation, and Cosine Similarity to serve accurate responses from a custom knowledge base, and a fully custom frontend built in HTML, CSS, and JavaScript. Problem to Solve Modern LLM-based bots are powerful, but they come with risks: Hallucinated responses. Inconsistent tone. Data privacy concerns. Many small businesses actually need a cheaper, smaller, safer, and more deterministic chatbot. This project addresses that gap. Backend Tech Stack Python 3 Flask (web server and API) Scikit-learn (for machine learning models) Pandas / Numpy (for data manipulation) TF-IDF vectorisation (for text representation) SVM (Support Vector Machine) (for classification) Cosine Similarity (for fine-grained matching) Workflow and the Maths Behind It Preprocessing the Knowledge Base The core of the chatbot is a CSV file containing common customer support questions and answers for a bank. Before training: Stemming: Normalise words like "running" to "run". TF-IDF Vectorisation: Convert text into a matrix that captures the importance of terms. Here’s a simple example of the preprocessing pipeline: `from sklearn.feature_extraction.text import TfidfVectorizer from nltk.stem import PorterStemmer Initialize stemmer stemmer = PorterStemmer() def stem_text(text): return ' '.join([stemmer.stem(word) for word in text.split()]) Example text questions = ["How do I reset my password?", "Forgot password help"] Apply stemming stemmed_questions = [stem_text(q) for q in questions] TF-IDF vectorisation vectorizer = TfidfVectorizer() X = vectorizer.fit_transform(stemmed_questions)` Classifying User Queries Once the data is preprocessed, I trained a Support Vector Machine (SVM) classifier with a linear kernel: `from sklearn.svm import SVC Train SVM clf = SVC(kernel='linear') clf.fit(X, labels) # labels are the class/category for each question` When a user submits a query: It’s preprocessed the same way (stemming + TF-IDF). Then classified into a question category. Fine-Grained Matching within the Class After finding the class, I zoom into the subset of related questions and use Cosine Similarity to identify the closest match: `from sklearn.metrics.pairwise import cosine_similarity Vectorize user query user_query_vector = vectorizer.transform([stem_text(user_query)]) Compare against subset of questions similarities = cosine_similarity(user_query_vector, subset_vectors) # subset_vectors are TF-IDF vectors of questions in the predicted class Find the best match best_idx = similarities.argmax() best_answer = subset_answers[best_idx]` This ensures fast and accurate matching even if user phrasing varies. Handling Escalations: Automated Support Ticket System Even with the best machine learning models, some user queries might not be answered perfectly. To handle these cases professionally, I built an automated support ticket creation system inside the chatbot flow. When a user indicates that the chatbot's answer was not helpful, the bot automatically initiates a support ticket creation dialogue. How the Support Ticket Flow Works Ask for the user's email: The bot prompts the user to input a valid email address. A simple email format check (@ and . present) is performed. Confirm or Update the Question: The user can either confirm the original query or provide additional detailed information. Generate and Log the Ticket: A unique ticket ID is created based on the timestamp. The ticket is saved using a helper function (save_support_ticket()). An email notification is sent to the user using send_email_notification() and sent to the support team. Handle Errors Gracefully: If something fails, users are informed politely, and the system logs the error and support ticket locally for admins. Sample code: `def handle_support_ticket_flow(user_message, step, ticket_data): if step == 1: # Collect email if "@" in user_message and "." in user_message: ticket_data["email"] = user_message return prompt_for_more_details(ticket_data) else: return prompt_for_valid_email(ticket_data) elif step == 2: # Confirm question or request more details if user_message.lower() == 'yes': return ask_for_detailed_question(ticket_data) elif user_message.lower() == 'no': return create_ticket(ticket_data) else: return prompt_for_yes_or_no(ticket_data) elif step == 3: # Save detailed user input as ticket return create_ticket_with_details(ticket_data, user_message

In customer support, fast and reliable answers matter. I wanted to create a controlled, efficient support chatbot — one that doesn't rely on generative AI but instead uses traditional machine learning, information retrieval, and a custom-designed user interface for an interactive, predictible experience.
In this project, I built a Flask web application that uses SVM classification, TF-IDF vectorisation, and Cosine Similarity to serve accurate responses from a custom knowledge base, and a fully custom frontend built in HTML, CSS, and JavaScript.
Problem to Solve
Modern LLM-based bots are powerful, but they come with risks:
- Hallucinated responses.
- Inconsistent tone.
- Data privacy concerns.
Many small businesses actually need a cheaper, smaller, safer, and more deterministic chatbot.
This project addresses that gap.
Backend Tech Stack
- Python 3
- Flask (web server and API)
- Scikit-learn (for machine learning models)
- Pandas / Numpy (for data manipulation)
- TF-IDF vectorisation (for text representation)
- SVM (Support Vector Machine) (for classification)
- Cosine Similarity (for fine-grained matching)
Workflow and the Maths Behind It
- Preprocessing the Knowledge Base The core of the chatbot is a CSV file containing common customer support questions and answers for a bank.
Before training:
Stemming: Normalise words like "running" to "run".
TF-IDF Vectorisation: Convert text into a matrix that captures the importance of terms.
Here’s a simple example of the preprocessing pipeline:
`from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.stem import PorterStemmer
Initialize stemmer
stemmer = PorterStemmer()
def stem_text(text):
return ' '.join([stemmer.stem(word) for word in text.split()])
Example text
questions = ["How do I reset my password?", "Forgot password help"]
Apply stemming
stemmed_questions = [stem_text(q) for q in questions]
TF-IDF vectorisation
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(stemmed_questions)`
- Classifying User Queries Once the data is preprocessed, I trained a Support Vector Machine (SVM) classifier with a linear kernel:
`from sklearn.svm import SVC
Train SVM
clf = SVC(kernel='linear')
clf.fit(X, labels) # labels are the class/category for each question`
When a user submits a query:
It’s preprocessed the same way (stemming + TF-IDF).
Then classified into a question category.
- Fine-Grained Matching within the Class After finding the class, I zoom into the subset of related questions and use Cosine Similarity to identify the closest match:
`from sklearn.metrics.pairwise import cosine_similarity
Vectorize user query
user_query_vector = vectorizer.transform([stem_text(user_query)])
Compare against subset of questions
similarities = cosine_similarity(user_query_vector, subset_vectors) # subset_vectors are TF-IDF vectors of questions in the predicted class
Find the best match
best_idx = similarities.argmax()
best_answer = subset_answers[best_idx]`
This ensures fast and accurate matching even if user phrasing varies.
Handling Escalations: Automated Support Ticket System
Even with the best machine learning models, some user queries might not be answered perfectly. To handle these cases professionally, I built an automated support ticket creation system inside the chatbot flow. When a user indicates that the chatbot's answer was not helpful, the bot automatically initiates a support ticket creation dialogue.
How the Support Ticket Flow Works
- Ask for the user's email:
- The bot prompts the user to input a valid email address. A simple email format check (@ and . present) is performed.
- Confirm or Update the Question: The user can either confirm the original query or provide additional detailed information.
- Generate and Log the Ticket: A unique ticket ID is created based on the timestamp.
- The ticket is saved using a helper function (save_support_ticket()).
- An email notification is sent to the user using send_email_notification() and sent to the support team.
- Handle Errors Gracefully: If something fails, users are informed politely, and the system logs the error and support ticket locally for admins.
Sample code:
`def handle_support_ticket_flow(user_message, step, ticket_data):
if step == 1: # Collect email
if "@" in user_message and "." in user_message:
ticket_data["email"] = user_message
return prompt_for_more_details(ticket_data)
else:
return prompt_for_valid_email(ticket_data)
elif step == 2: # Confirm question or request more details
if user_message.lower() == 'yes':
return ask_for_detailed_question(ticket_data)
elif user_message.lower() == 'no':
return create_ticket(ticket_data)
else:
return prompt_for_yes_or_no(ticket_data)
elif step == 3: # Save detailed user input as ticket
return create_ticket_with_details(ticket_data, user_message)`
Each step is stateful — meaning the chatbot remembers where the user is in the conversation flow — and the system automatically progresses based on user input.
This support ticket system enables the user to contact the support team correctly if the bot fails to answer their query to their liking. By combining smart automation with human fallback options, I built a chatbot that doesn't just answer questions — it ensures no customer is ever left behind.
Frontend Tech Stack
- HTML5 (structure)
- CSS3 (styling, animations)
- Vanilla JavaScript (dynamic behavior)
- Google Fonts and Material Symbols (icons)
- Responsive Design (for mobile and desktop
Chatbot UI Features
- Clean, Toggleable Interface The chatbot toggles open and closed with a floating button, creating a minimal footprint when not in use.
I used CSS transitions for smooth animations between states.
- Modern, Card-Style Chat Window The main chat interface is styled like a mobile chat app:
Bank Support Team
send
Messages are dynamically appended to the chatbox using JavaScript.
- Fully Responsive Design On mobile screens, the chatbot resizes to full-screen mode automatically for better usability.
Example CSS snippet:
@media (max-width: 490px) {
.chatbot {
right: 0;
bottom: 0;
height: 100%;
border-radius: 0;
width: 100%;
}
}
- Asynchronous Communication with Backend When the user submits a message, JavaScript sends an HTTP request to the Flask server:
async function getBotResponse(userText) {
const response = await fetch("/chat", {
method: "POST",
body: JSON.stringify({ message: userText }),
headers: { "Content-Type": "application/json" }
});
const data = await response.json();
return data.reply;
}
The chatbot displays the server’s reply without reloading the page, creating a smooth, real-time experience.
Design Philosophy
- Speed: Lightweight frontend and backend.
- Simplicity: No frameworks; pure HTML/CSS/JS for maximum control.
- Professionalism: Clean UX and error handling.
- Expandability: Easy to add new questions, classes, or UI features.
What I Learned
- Classic machine learning techniques can deliver incredible value in well-defined domains.
- Frontend design plays a crucial role in user trust and experience.
- Building debug tools and fallback strategies improves reliability.
- Full-stack integration (Flask + Vanilla JS) made me appreciate how different layers must work together for a seamless product.
Final Thoughts
This project shows that you don't always need the biggest AI models to solve real problems.
With thoughtful backend logic and a polished, user-friendly frontend, you can build robust, safe, and beautiful ML experiences.