Building a Complete Chatbot with LangChain

This topic brings together everything from the course — models, prompts, memory, document loading, embeddings, vector stores, RAG, and output parsers — into a single working chatbot application. By the end you will have a fully functional AI assistant that answers questions from your own documents, remembers the conversation, handles errors gracefully, and runs as a command-line application ready to be extended into a web service.

The Application We Are Building

The chatbot is a knowledge base assistant for a fictional company called NovaTech. It reads from company documents (FAQ, policy guide, product catalog), answers employee and customer questions using those documents, and remembers the conversation context across multiple turns. When the answer is not in the documents, it says so clearly instead of making something up.

NovaTech Assistant Architecture:

                  User Question
                       │
                       ▼
              ┌─────────────────┐
              │  Input Handler  │  (validates, classifies question)
              └────────┬────────┘
                       │
          ┌────────────┴────────────┐
          │                         │
          ▼                         ▼
  ┌──────────────┐        ┌──────────────────┐
  │  Rephrase    │        │  Direct Answer   │
  │  with        │        │  (greetings,     │
  │  history     │        │   commands)      │
  └──────┬───────┘        └──────────────────┘
         │
         ▼
  ┌──────────────┐
  │  Retriever   │  (searches company knowledge base)
  └──────┬───────┘
         │ relevant chunks
         ▼
  ┌──────────────┐
  │  RAG Chain   │  (answers using retrieved context)
  └──────┬───────┘
         │
         ▼
  ┌──────────────┐
  │  Memory      │  (saves turn to history)
  └──────┬───────┘
         │
         ▼
    Answer to User

Project Structure

novatech_bot/
├── .env                    ← API keys
├── .gitignore
├── requirements.txt
├── documents/
│   ├── faq.txt
│   ├── policies.txt
│   └── products.txt
├── knowledge_base/         ← FAISS vector store (generated)
├── config.py               ← Settings and constants
├── knowledge_base_builder.py  ← Index documents
├── chains.py               ← All LangChain chains
└── chatbot.py              ← Main application entry point

Step 1: Configuration (config.py)

# config.py

# Model settings
CHAT_MODEL = "gpt-3.5-turbo"
EMBEDDING_MODEL = "text-embedding-ada-002"
TEMPERATURE = 0.2
MAX_TOKENS = 800

# Retrieval settings
CHUNK_SIZE = 800
CHUNK_OVERLAP = 150
TOP_K_RESULTS = 4

# Memory settings
MAX_HISTORY_MESSAGES = 20   # Keep last 20 messages (10 turns)

# Paths
DOCUMENTS_DIR = "./documents"
VECTOR_STORE_PATH = "./knowledge_base"

# Bot identity
BOT_NAME = "Nova"
SYSTEM_PROMPT = """You are Nova, the NovaTech AI assistant. Your job is to help
employees and customers by answering questions based on NovaTech's official documents.

Rules:
- Answer ONLY using the provided context documents.
- If the answer is not in the context, say: "I don't have that information in my documents. Please contact support@novatech.com."
- Be friendly, professional, and concise.
- Always cite which document your answer comes from when possible.
- Do not make up information, prices, policies, or features."""

Step 2: Create Sample Documents (documents/)

# documents/faq.txt
Frequently Asked Questions - NovaTech

Q: What are your business hours?
A: NovaTech support is available Monday to Friday, 9am to 6pm IST.
   Emergency support is available 24/7 for Enterprise customers.

Q: How do I reset my password?
A: Visit account.novatech.com and click "Forgot Password". Enter your registered
   email address. You will receive a reset link within 5 minutes.

Q: What payment methods do you accept?
A: We accept Visa, Mastercard, UPI, and bank transfers. All payments are processed
   securely through Razorpay.
# documents/policies.txt
NovaTech Company Policies

REFUND POLICY:
Customers may request a full refund within 14 days of purchase for any reason.
After 14 days, refunds are considered on a case-by-case basis.
To initiate a refund, email billing@novatech.com with your order number.

REMOTE WORK POLICY (Employee):
Full-time employees may work remotely up to 3 days per week with manager approval.
New employees must work from office for their first 60 days.
Remote work equipment allowance: Rs 15,000 per year.

LEAVE POLICY (Employee):
Annual leave: 18 days per year.
Sick leave: 12 days per year, no carry-forward.
Maternity leave: 26 weeks fully paid.
Paternity leave: 15 days fully paid.
# documents/products.txt
NovaTech Product Catalog

NOVA STARTER PLAN:
Price: Rs 999/month
Users: Up to 5
Storage: 20 GB
Features: Basic analytics, email support, API access (100k calls/month)

NOVA BUSINESS PLAN:
Price: Rs 3,999/month
Users: Up to 25
Storage: 200 GB
Features: Advanced analytics, priority support, API access (1M calls/month), custom integrations

NOVA ENTERPRISE PLAN:
Price: Custom pricing
Users: Unlimited
Storage: Unlimited
Features: All Business features, dedicated account manager, SLA guarantee, on-premise option
Contact: sales@novatech.com

Step 3: Build the Knowledge Base (knowledge_base_builder.py)

# knowledge_base_builder.py

import os
from dotenv import load_dotenv
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from config import (
    DOCUMENTS_DIR, VECTOR_STORE_PATH,
    CHUNK_SIZE, CHUNK_OVERLAP, EMBEDDING_MODEL
)

load_dotenv()

def build_knowledge_base():
    print("Building NovaTech knowledge base...")

    # Load all text files from documents folder
    loader = DirectoryLoader(
        DOCUMENTS_DIR,
        glob="**/*.txt",
        loader_cls=TextLoader,
        show_progress=True
    )
    documents = loader.load()
    print(f"Loaded {len(documents)} documents")

    # Split into chunks
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP
    )
    chunks = splitter.split_documents(documents)
    print(f"Created {len(chunks)} chunks")

    # Add source category to metadata
    for chunk in chunks:
        filename = os.path.basename(chunk.metadata.get("source", ""))
        if "faq" in filename:
            chunk.metadata["category"] = "FAQ"
        elif "policies" in filename:
            chunk.metadata["category"] = "Policy"
        elif "products" in filename:
            chunk.metadata["category"] = "Product"

    # Build and save vector store
    embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
    vector_store = FAISS.from_documents(chunks, embeddings)
    vector_store.save_local(VECTOR_STORE_PATH)

    print(f"Knowledge base saved to {VECTOR_STORE_PATH}")
    print("Done! Run chatbot.py to start the assistant.")

if __name__ == "__main__":
    build_knowledge_base()

Step 4: Build the Chains (chains.py)

# chains.py

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel, RunnableLambda
from config import (
    CHAT_MODEL, EMBEDDING_MODEL, TEMPERATURE, MAX_TOKENS,
    TOP_K_RESULTS, VECTOR_STORE_PATH, SYSTEM_PROMPT
)

load_dotenv()

# Initialize components
model = ChatOpenAI(model=CHAT_MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS)
embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
parser = StrOutputParser()

def load_retriever():
    store = FAISS.load_local(
        VECTOR_STORE_PATH, embeddings,
        allow_dangerous_deserialization=True
    )
    return store.as_retriever(
        search_type="mmr",
        search_kwargs={"k": TOP_K_RESULTS, "fetch_k": 15}
    )

def format_docs(docs) -> str:
    """Format retrieved chunks into a readable context block with source labels."""
    sections = []
    for doc in docs:
        category = doc.metadata.get("category", "Document")
        source = doc.metadata.get("source", "unknown")
        sections.append(f"[{category} — {source}]\n{doc.page_content}")
    return "\n\n---\n\n".join(sections)

# Chain 1: Rephrase follow-up questions using conversation history
rephrase_prompt = ChatPromptTemplate.from_messages([
    ("system",
     "Rephrase the user's question to be fully standalone using the conversation history. "
     "Return only the rephrased question. Preserve the original intent exactly."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{question}")
])
rephrase_chain = rephrase_prompt | model | parser

# Chain 2: Answer using retrieved context
answer_prompt = ChatPromptTemplate.from_messages([
    ("system", SYSTEM_PROMPT + "\n\nContext from NovaTech documents:\n{context}"),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{question}")
])
answer_chain = answer_prompt | model | parser

retriever = None  # Loaded lazily to avoid errors before KB is built

def get_retriever():
    global retriever
    if retriever is None:
        retriever = load_retriever()
    return retriever

def build_rag_response(question: str, history: list) -> dict:
    """Full RAG pipeline: retrieve context and generate answer."""
    r = get_retriever()
    docs = r.invoke(question)
    context = format_docs(docs)

    answer = answer_chain.invoke({
        "context": context,
        "history": history,
        "question": question
    })

    sources = list(set(
        doc.metadata.get("category", "Unknown")
        for doc in docs
    ))

    return {"answer": answer, "sources": sources, "docs": docs}

Step 5: Main Chatbot Application (chatbot.py)

# chatbot.py

import os
from pathlib import Path
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage, AIMessage
from chains import rephrase_chain, build_rag_response
from config import BOT_NAME, VECTOR_STORE_PATH, MAX_HISTORY_MESSAGES

load_dotenv()

# Conversation state
history = []

COMMANDS = {
    "/clear": "Conversation history cleared.",
    "/help": "Commands: /clear, /help, /history",
    "/history": None  # Handled dynamically
}

def trim_history(history: list) -> list:
    """Keep only the last MAX_HISTORY_MESSAGES messages."""
    if len(history) > MAX_HISTORY_MESSAGES:
        return history[-MAX_HISTORY_MESSAGES:]
    return history

def handle_command(cmd: str) -> str | None:
    if cmd == "/clear":
        history.clear()
        return COMMANDS["/clear"]
    if cmd == "/help":
        return COMMANDS["/help"]
    if cmd == "/history":
        if not history:
            return "No conversation history yet."
        lines = []
        for msg in history[-10:]:  # Show last 10 messages
            speaker = "You" if msg.type == "human" else BOT_NAME
            lines.append(f"{speaker}: {msg.content[:80]}...")
        return "\n".join(lines)
    return None

def chat(user_input: str) -> str:
    """Main chat function: routes input through rephrase → retrieve → answer."""
    # Rephrase if there is history
    if history:
        try:
            standalone_q = rephrase_chain.invoke({
                "history": history,
                "question": user_input
            })
        except Exception:
            standalone_q = user_input  # Fall back to original if rephrase fails
    else:
        standalone_q = user_input

    # Retrieve and answer
    try:
        result = build_rag_response(standalone_q, history)
        answer = result["answer"]
        sources = result["sources"]

        # Append source footnote if sources were found
        if sources:
            answer += f"\n\n[Sources: {', '.join(sources)}]"

    except FileNotFoundError:
        return ("Knowledge base not found. Please run knowledge_base_builder.py first.")
    except Exception as e:
        return f"I encountered an error. Please try again. (Error: {type(e).__name__})"

    # Save to history
    history.append(HumanMessage(content=user_input))
    history.append(AIMessage(content=answer))

    # Trim to keep history manageable
    trimmed = trim_history(history)
    history.clear()
    history.extend(trimmed)

    return answer

def main():
    """Run the chatbot in the terminal."""
    print(f"\n{'='*60}")
    print(f"  {BOT_NAME} — NovaTech AI Assistant")
    print(f"{'='*60}")
    print(f"  Type your question and press Enter.")
    print(f"  Commands: /help | /clear | /history | quit")
    print(f"{'='*60}\n")

    # Check knowledge base exists
    if not Path(VECTOR_STORE_PATH).exists():
        print(f"WARNING: Knowledge base not found at {VECTOR_STORE_PATH}")
        print("Run: python knowledge_base_builder.py\n")
        return

    while True:
        try:
            user_input = input("You: ").strip()
        except (KeyboardInterrupt, EOFError):
            print(f"\n{BOT_NAME}: Goodbye!")
            break

        if not user_input:
            continue

        if user_input.lower() in ["quit", "exit", "bye"]:
            print(f"{BOT_NAME}: Goodbye! Have a great day.")
            break

        if user_input.startswith("/"):
            result = handle_command(user_input.lower())
            if result is not None:
                print(f"{BOT_NAME}: {result}\n")
                continue
            else:
                print(f"{BOT_NAME}: Unknown command. Type /help for available commands.\n")
                continue

        response = chat(user_input)
        print(f"\n{BOT_NAME}: {response}\n")

if __name__ == "__main__":
    main()

Running the Application

Follow these steps to run the complete chatbot:

# Step 1: Install dependencies
pip install langchain langchain-openai langchain-community faiss-cpu python-dotenv

# Step 2: Set up .env with your API key
echo "OPENAI_API_KEY=sk-your-key-here" > .env

# Step 3: Create the documents folder and add your text files
mkdir documents
# (copy the sample faq.txt, policies.txt, products.txt files shown above)

# Step 4: Build the knowledge base (do this once, or whenever documents change)
python knowledge_base_builder.py

# Step 5: Start the chatbot
python chatbot.py

Sample Conversation

You: Hi, what are your business hours?
Nova: NovaTech support is available Monday to Friday, 9am to 6pm IST.
      Enterprise customers have access to 24/7 emergency support.
      [Sources: FAQ]

You: What about refunds?
Nova: You can request a full refund within 14 days of purchase for any reason.
      After 14 days, refunds are reviewed case by case. To start a refund,
      email billing@novatech.com with your order number.
      [Sources: Policy]

You: How much does the business plan cost?
Nova: The Nova Business Plan is priced at Rs 3,999 per month. It supports up to
      25 users, 200 GB of storage, advanced analytics, priority support, and
      1 million API calls per month.
      [Sources: Product]

You: What about leave for new parents?
Nova: NovaTech offers 26 weeks of fully paid maternity leave and 15 days of
      fully paid paternity leave.
      [Sources: Policy]

You: Can I work from home?
Nova: Full-time employees can work remotely up to 3 days per week with manager
      approval. New employees must work from the office for their first 60 days.
      The annual remote work equipment allowance is Rs 15,000.
      [Sources: Policy]

Extending the Chatbot

Add More Document Types

Add PDF loaders to knowledge_base_builder.py to include product manuals, presentations, or reports. The rest of the application works without changes.

Add a Web Interface

Replace the terminal loop in chatbot.py with a FastAPI or Flask endpoint. The chat() function becomes the API handler — it accepts a string and returns a string, making the transition trivial.

Add a WhatsApp or Telegram Bot

Use the Twilio WhatsApp API or python-telegram-bot library. Both call your chat() function with the incoming message and send back the response. The LangChain logic never changes.

Add Streaming for Web UIs

Replace answer_chain.invoke() with answer_chain.stream() and yield chunks to the frontend via server-sent events. Users see words appearing in real time.

Summary

A complete production chatbot combines document loading, text splitting, embedding, vector storage, retrieval-augmented generation, conversation memory, input validation, command handling, and error resilience. The modular project structure separates configuration, chain logic, and application flow into distinct files. The knowledge base is built once and reused across many sessions. The chat function handles rephrasing, retrieval, answering, and memory management in a clean sequence.

Leave a Comment