Step-by-step builds

Chatbot Project in Python With Source Code: Intent Bot Plus LLM Fallback

Satvik ParamkushamUpdated October 5, 202613 min read

Quick answer

A good chatbot project in Python has two layers. First, a scikit-learn model classifies each message into an intent such as fees or admissions and replies from a JSON file. Second, when the model is not confident, the message goes to an LLM API. You get fast, controlled answers for common questions and sensible replies for everything else.

Most chatbot tutorials stop at one of two extremes. Either they build a rule-based bot that breaks the moment a user types something unexpected, or they wrap an LLM API in ten lines and call it a project. Neither gives you much to explain in a viva or an interview.

This guide builds the version that sits in between, and it is how many real support bots work: an intent classifier answers the common questions from approved replies, and an LLM handles the long tail. The example is a college helpdesk bot that answers questions about fees, admissions, timings and placements, but you can swap in any domain by editing one JSON file.

You will write about 100 lines of Python across a few small files, run it in the terminal first, then put a Streamlit chat interface on top. Every step has code you can copy, run and change.

What is a chatbot project in Python?

A chatbot project in Python is a program that reads a user's message, works out what they want, and sends back a useful reply. The interesting part is the "works out what they want" step, which is where machine learning comes in.

The bot in this guide uses intent classification. You define a handful of intents (greeting, fees, admissions, timings, placements, goodbye), give each one 6 to 10 example phrases and a few approved replies, and train a small text classifier on those phrases. When a new message arrives, the classifier predicts the intent and a confidence score.

High confidence means the bot replies from your JSON file: fast, free and exactly the wording you approved. Low confidence means the message is outside what you trained for, so the bot hands it to a large language model through an API.

What do you need to build a chatbot in Python?

You need Python basics, any laptop, and (for the second stage only) an API key for a chat model. No GPU, no big dataset.

PartWhat we useWhy
LanguagePython 3.11 or newerCurrent scikit-learn releases require it
Training dataA small intents.json file you writeYou control every intent and reply
Intent modelscikit-learn: TF-IDF plus Logistic RegressionTrains in under a second, gives probabilities
FallbackAny chat model APIThe example uses the OpenAI Python SDK; Gemini, Groq or a local model work the same way
InterfaceTerminal first, then StreamlitTest fast, then demo in a browser

You should be comfortable with dictionaries, lists, functions and reading JSON in Python. If you are preparing for placements as well, our Python for AI interview questions cover the same basics this project uses.

How does an intent-based chatbot with an LLM fallback work?

The bot runs the same four steps for every message:

  1. Clean the text: lowercase it and strip extra spaces.
  2. Classify the intent: the TF-IDF vectorizer turns the message into numbers, and Logistic Regression returns a probability for each intent.
  3. Decide the route: if the top probability is above a threshold (we start at 0.5), reply with a random approved response for that intent.
  4. Fall back: otherwise, send the message plus the last few turns of conversation to an LLM with a short system prompt, and return its reply.

The threshold is the single most important design choice in the project. Set it too low and the bot gives confident wrong answers from the JSON file. Set it too high and almost everything goes to the LLM, which costs money and loses your approved wording. Step 8 shows how to tune it with numbers instead of guessing.

How to build a chatbot in Python, step by step

  1. Step 1. Set up the project

    Create a folder and a virtual environment, then install four packages.

    mkdir college-chatbot && cd college-chatbot
    python -m venv .venv
    # Windows: .venv\Scripts\activate    macOS/Linux: source .venv/bin/activate
    pip install scikit-learn joblib openai streamlit

    Set your API key as an environment variable named OPENAI_API_KEY (or whatever your provider uses). Never paste a key into your code or push it to GitHub. You can build and test the whole first stage without any key at all.

  2. Step 2. Write the intents file

    The intents file is your training data and your reply bank in one place. Each intent has a tag, example user phrases (patterns) and approved replies (responses).

    {
      "intents": [
        {"tag": "greeting",
         "patterns": ["hi", "hello", "hey there", "good morning", "namaste", "is anyone there"],
         "responses": ["Hello! Ask me about fees, admissions, timings or placements."]},
        {"tag": "fees",
         "patterns": ["what is the fee", "how much is the btech fee", "fee structure",
                      "hostel fees", "can i pay fees in instalments", "fees kitna hai"],
         "responses": ["The fee structure is on the college website under Admissions, then Fees."]},
        {"tag": "admissions",
         "patterns": ["how do i apply", "admission process", "last date to apply",
                      "documents needed for admission", "is there an entrance exam"],
         "responses": ["Applications open on the college portal. Keep your marksheets and ID ready."]},
        {"tag": "timings",
         "patterns": ["what are the college timings", "when does college start",
                      "library timings", "is college open on saturday", "office hours"],
         "responses": ["Classes run 9 am to 4 pm, Monday to Friday. The office closes at 5 pm."]},
        {"tag": "goodbye",
         "patterns": ["bye", "thanks", "thank you", "see you", "that is all"],
         "responses": ["Glad to help. All the best!"]}
      ]
    }

    Save it as intents.json. Add a placements intent the same way. Aim for 6 to 10 varied patterns per intent, including the short, misspelled and Hinglish ways real students type. Variety in patterns matters far more than the number of intents.

  3. Step 3. Train the intent classifier with scikit-learn

    A scikit-learn pipeline chains the vectorizer and the classifier so you can save and load them as one object.

    # train.py
    import json
    import joblib
    from sklearn.pipeline import make_pipeline
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.linear_model import LogisticRegression
    
    with open("intents.json", encoding="utf-8") as f:
        data = json.load(f)
    
    X, y = [], []
    for intent in data["intents"]:
        for pattern in intent["patterns"]:
            X.append(pattern.lower())
            y.append(intent["tag"])
    
    model = make_pipeline(
        TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 4)),
        LogisticRegression(max_iter=1000, C=10),
    )
    model.fit(X, y)
    joblib.dump(model, "intent_model.joblib")
    print(f"Trained on {len(X)} phrases across {len(set(y))} intents")

    Two choices worth explaining. Character n-grams (analyzer="char_wb") match pieces of words, so "admision" and "admission" still look similar, which helps with typos and mixed spellings. A higher C value makes the classifier more confident on a tiny dataset, which keeps the threshold in Step 4 meaningful. Run python train.py whenever you edit intents.json.

  4. Step 4. Predict the intent and route by confidence

    The bot asks the model for probabilities, not just a label, so it knows when it is unsure.

    # bot.py
    import json
    import random
    import joblib
    
    model = joblib.load("intent_model.joblib")
    with open("intents.json", encoding="utf-8") as f:
        RESPONSES = {i["tag"]: i["responses"] for i in json.load(f)["intents"]}
    
    THRESHOLD = 0.5
    
    def classify(text):
        probs = model.predict_proba([text.lower().strip()])[0]
        best = probs.argmax()
        return model.classes_[best], float(probs[best])
    
    def reply(text, history):
        tag, confidence = classify(text)
        if confidence >= THRESHOLD:
            return random.choice(RESPONSES[tag]), f"intent: {tag} ({confidence:.2f})"
        return llm_fallback(text, history), f"llm (best guess {tag}, {confidence:.2f})"

    The reply function returns the route as well as the answer. Showing "intent: fees (0.83)" next to each reply during testing tells you instantly whether the classifier or the LLM answered, and why.

  5. Step 5. Add the LLM fallback

    Add this to bot.py. The system prompt keeps the LLM in role and, just as important, stops it from inventing college-specific facts it was never given.

    import os
    from openai import OpenAI
    
    client = OpenAI()  # reads OPENAI_API_KEY from the environment
    MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini")  # use any current chat model from your provider's docs
    SYSTEM = (
        "You are the helpdesk assistant for a college in India. Reply in two or three "
        "short sentences. If asked about specific fees, dates, cut-offs or rules, "
        "say you do not have that information and suggest contacting the college office."
    )
    
    def llm_fallback(text, history):
        messages = [{"role": "system", "content": SYSTEM}]
        messages += history[-6:]  # last three turns for context
        messages.append({"role": "user", "content": text})
        try:
            out = client.chat.completions.create(
                model=MODEL, messages=messages, temperature=0.3
            )
            return out.choices[0].message.content
        except Exception as err:
            return f"Sorry, I could not reach the AI service ({type(err).__name__})."

    Passing the last few turns of history lets the bot handle follow-ups like "and for MBA?". A low temperature keeps replies short and steady. The instruction about fees and dates is your defence against AI hallucination: an LLM will happily make up a fee amount if you let it.

  6. Step 6. Chat in the terminal

    A terminal loop is the fastest way to test. Keep it as a separate file so the bot logic stays reusable.

    # chat.py
    from bot import reply
    
    history = []
    print("College helpdesk bot. Type 'quit' to exit.")
    while True:
        text = input("You: ").strip()
        if not text:
            continue
        if text.lower() in {"quit", "exit"}:
            print("Bot: Goodbye!")
            break
        answer, route = reply(text, history)
        print(f"Bot: {answer}   [{route}]")
        history += [
            {"role": "user", "content": text},
            {"role": "assistant", "content": answer},
        ]

    Run python chat.py and try "fees kitna hai", "hostel fess" (with the typo) and "can you explain recursion". The first two should hit the fees intent; the third should route to the LLM.

  7. Step 7. Build the Streamlit chat interface

    Streamlit has chat elements built in, so a browser UI takes about 20 lines. Session state keeps the conversation across reruns.

    # app.py
    import streamlit as st
    from bot import reply
    
    st.title("College Helpdesk Bot")
    
    if "history" not in st.session_state:
        st.session_state.history = []
    
    for msg in st.session_state.history:
        st.chat_message(msg["role"]).write(msg["content"])
    
    if text := st.chat_input("Ask about fees, admissions, timings..."):
        st.chat_message("user").write(text)
        answer, route = reply(text, st.session_state.history)
        with st.chat_message("assistant"):
            st.write(answer)
            st.caption(f"Answered by {route}")
        st.session_state.history += [
            {"role": "user", "content": text},
            {"role": "assistant", "content": answer},
        ]

    Run it with streamlit run app.py. The caption under each reply makes your demo far more convincing, because the examiner can see the routing decision happen live.

  8. Step 8. Evaluate the classifier and tune the threshold

    Ask a friend to write 30 test questions you have never seen: 20 that belong to your intents and 10 that do not (general questions the LLM should get). Then measure.

    # evaluate.py
    import joblib
    from sklearn.metrics import classification_report
    
    model = joblib.load("intent_model.joblib")
    in_scope = [("btech fee kitni hai", "fees"), ("when is the last date to apply", "admissions"),
                ("library open on sunday?", "timings"), ("hello", "greeting")]  # add your 20
    out_of_scope = ["what is machine learning", "write a poem about rain"]  # add your 10
    
    texts, labels = zip(*in_scope)
    print(classification_report(labels, model.predict(list(texts)), zero_division=0))
    
    for t in [0.3, 0.4, 0.5, 0.6, 0.7]:
        kept = sum(model.predict_proba([q])[0].max() >= t for q in texts)
        leaked = sum(model.predict_proba([q])[0].max() >= t for q in out_of_scope)
        print(f"threshold {t}: in-scope answered {kept}/{len(texts)}, "
              f"out-of-scope wrongly answered {leaked}/{len(out_of_scope)}")

    Pick the threshold where in-scope coverage is high and out-of-scope leaks are near zero. Put that table in your report. If a specific intent scores poorly, add more varied patterns to it rather than more intents.

What chatbot project ideas can you build with this code?

Change intents.json and the system prompt, and the same code becomes a different project:

  • Healthcare appointment bot: intents for timings, booking, departments and emergency numbers. The LLM fallback must refuse medical advice and point to a doctor. Say so clearly in your report.
  • Railway or bus enquiry bot: intents for PNR status, cancellation rules and luggage. Upgrade it by calling a real API for live data.
  • E-commerce support bot: order status, returns, refunds and delivery questions.
  • Bank FAQ bot: account opening, KYC documents, card blocking. Never let the LLM handle anything involving account numbers.
  • Hinglish customer care bot: train patterns in Hinglish and test how the character n-gram model copes with mixed spellings.

Upgrades that add real depth: replace TF-IDF with sentence embeddings for better paraphrase matching, add entity extraction (course name, semester), log every low-confidence message to grow your training data, or add guardrails on the LLM output.

The natural next step is answering from documents instead of a fixed reply list. That is the RAG chatbot project, which reuses everything you built here.

How do you explain a chatbot project in a viva or interview?

Start with one sentence: "My chatbot classifies each message into an intent using TF-IDF and Logistic Regression, answers confident predictions from approved replies, and sends low-confidence messages to an LLM, with a threshold I tuned on a held-out test set."

Then prepare for these follow-ups:

  • Why Logistic Regression and not a neural network? (Tiny dataset, fast, gives calibrated-enough probabilities, easy to explain.)
  • What is TF-IDF, and why character n-grams?
  • How did you choose the threshold? (Show the Step 8 table.)
  • What stops the LLM from inventing facts? (System prompt, low temperature, and keeping factual answers in the JSON.)
  • How would you add a new intent? (Edit JSON, retrain, re-evaluate.)
  • What happens with overfitting on 50 training phrases? (It is a real risk, which is exactly why you test on phrases you did not write.)

Our machine learning interview questions page covers classification, metrics and overfitting in more depth.

Common mistakes in student chatbot projects

Five mistakes show up in most weak submissions:

  1. Using predict instead of predict_proba. Without a confidence score the bot always picks some intent, even for "write me a poem".
  2. Testing on the training phrases. Of course they score 100 percent. Test on phrases someone else wrote.
  3. Too few, too similar patterns. "what is the fee", "what is the fees" and "what's the fee" are one example, not three.
  4. API key in the code. Keys pushed to public GitHub repos can be found and misused quickly. Use environment variables.
  5. Letting the LLM answer factual policy questions. It will sound confident and be wrong. Keep facts in your approved replies.

Fix these and your chatbot will already stand out from a typical submission.

Frequently asked questions

Where can I get a chatbot project in Python with source code?

The complete code is on this page: intents.json, train.py, bot.py, chat.py, app.py and evaluate.py. Copy each file, run them in order, and push the folder to your own GitHub with a README. Building it yourself is far better for a viva than downloading a zip you cannot explain.

Is a chatbot a good final-year project in Python?

A plain rule-based chatbot is usually too small for final year. A hybrid bot with an ML intent classifier, an LLM fallback, a tuned threshold and evaluation numbers is a solid project. Adding document retrieval, which turns it into a RAG chatbot, makes it stronger still.

Can I build an AI chatbot in Python without an API key?

Yes. The intent classifier stage needs no key and runs fully offline with scikit-learn. For the fallback, you can skip it and return a polite "I did not understand" message, or run a small open-source model locally if your laptop has enough memory.

What is the difference between a rule-based and an AI chatbot?

A rule-based chatbot matches exact keywords or patterns you write by hand, so it breaks on new phrasing. An AI chatbot learns from examples, so it handles phrasing it has never seen, and an LLM-based one can generate new replies. The bot in this guide combines a learned classifier with an LLM.

Should I use NLTK or ChatterBot for a Python chatbot project?

You do not need either. Older tutorials used NLTK for tokenising and ChatterBot for replies, but scikit-learn's TF-IDF handles the text processing here in one line and an LLM covers free-form replies. Fewer dependencies also means fewer installation problems on demo day, and every line of logic is yours to explain.

References