End-to-End LLM Q&A Chatbot with LangChain, Google Gemini, and Streamlit
A practical guide to building an interactive, conversational Question-and-Answer (Q&A) web application using Python, LangChain, Google's Gemini LLM family, and Streamlit. This project transitions from a basic single-turn CLI prompt to an interactive multi-turn terminal loop, and ultimately into an asynchronous, session-backed web chatbot capable of preserving chat history across browser re-renders.
1. Project Architecture and Technical Foundation
1.1 The Evolution from Script to Web Application
Building an LLM application requires three core components:
- The Model/Reasoning Engine: Google Gemini, accessed via LangChain's integration wrapper (
langchain-google-genai). - The Orchestration Framework: LangChain, which handles standard invocations, API communication, and payload parsing.
- The User Interface & State Manager: Streamlit, an open-source Python framework designed for rapid deployment of data science and AI applications.
1.2 System Execution Flow
The diagram below shows how user queries flow from the browser to Google's inference servers:
2. Environment Setup and Dependency Configuration
2.1 Virtual Environment Preparation
All code executes from within a dedicated application directory (e.g., apps/), isolated by a Python virtual environment.
Install the required core packages via pip:
pip install python-dotenv langchain-google-genai streamlit
To manage dependencies cleanly, define them inside a requirements.txt file:
python-dotenv
langchain-google-genai
streamlit
Install the dependencies:
pip install -r requirements.txt
2.2 Environment Variables Configuration
To authenticate with Google Gemini without hardcoding credentials into source control, define the API key in a .env file at the root of your project:
GOOGLE_API_KEY=your_actual_google_gemini_api_key_here
To ensure python-dotenv locates and injects these variables into the runtime environment:
from dotenv import load_dotenv
# Reads the .env file and updates os.environ
load_dotenv()
3. Step 1: Basic Single-Turn CLI Interaction
Before introducing web UI components or complex looping mechanisms, verify the basic connection by querying the Google Gemini model directly through LangChain.
import os
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI
# Load API credentials from .env
load_dotenv()
# Initialize the Gemini LLM
# Model identifier can be adjusted based on the current Gemini version (e.g., gemini-1.5-pro, gemini-1.5-flash, or gemini-pro)
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash")
# Define a single question
question = "Who is the Prime Minister of India?"
# Invoke the model directly
result = llm.invoke(question)
# The result is an AIMessage object; the generated text is stored in .content
print("AI Response:")
print(result.content)
Run the script from your terminal:
python 1_qna_bot.py
AI Response:
The current Prime Minister of India is Narendra Modi.
4. Step 2: Multi-Turn Terminal Bot with a While Loop
A single-turn script terminates immediately after producing an answer. To build an ongoing conversation in the terminal, wrap the invocation inside an infinite while True loop and add termination handlers (e.g., quit, exit, bye).
import os
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI
load_dotenv()
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash")
print("--- Terminal Q&A Bot Initialized (Type 'quit', 'exit', or 'bye' to stop) ---\n")
while True:
query = input("User: ")
# Process exit triggers (case-insensitive)
if query.strip().lower() in ["quit", "exit", "bye"]:
print("AI: Goodbye! 👋")
break
# Guard against empty submissions
if not query.strip():
continue
# Query LLM
response = llm.invoke(query)
# Print the model's text response
print(f"AI: {response.content}\n")
5. Transitioning to Streamlit: The Rerun Lifecycle Challenge
Streamlit simplifies Python web design by treating entire scripts as linear execution runs. However, this creates a specific state management challenge:
5.1 The Streamlit Execution Model
Whenever a user interacts with a widget (such as typing into an st.chat_input or clicking a button):
- The entire Python script executes from Line 1 to the end.
- Normal in-memory script variables are completely re-instantiated and wiped out.
- Without explicit persistence, previously generated chat messages vanish from memory upon every submission.
5.2 Real-Life Analogy: The Ephemeral Whiteboard vs. The Persistent Notebook
Real-Life Analogy
Think of Streamlit's default execution model like an ephemeral whiteboard in a conference room. Every time someone speaks (an interaction or input), a cleaner walks in, wipes the entire whiteboard completely blank, and writes down only the single sentence just spoken.
To keep track of an ongoing conversation, you cannot rely on the whiteboard alone. You need a persistent notebook kept safely in a drawer (
st.session_state). Every time the cleaner erases the board, you open your notebook, transcribe all past conversations back onto the board first, and then add the newest remark to both the board and the notebook.
6. Managing Persistent Memory via st.session_state
To retain conversation history across Streamlit's full script reruns, store messages inside st.session_state.
6.1 Initializing Session State
Check if the messages key exists in st.session_state. If not, initialize it as an empty list:
import streamlit as st
if "messages" not in st.session_state:
st.session_state.messages = []
6.2 Data Structure of Message Payloads
Chat entries are stored as a list of dictionaries, following standard chat conventions:
st.session_state.messages = [
{"role": "user", "content": "What is 2 + 2?"},
{"role": "ai", "content": "2 + 2 is 4."}
]
6.3 Re-rendering Historical Messages
Before accepting or rendering new input, iterate through the session state and redraw all historical messages:
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
7. Step 3: Complete End-to-End Streamlit Application
Here is the complete production-ready implementation of 1_qna_bot.py. It integrates python-dotenv, langchain-google-genai, and streamlit with session state persistence and dual-message rendering.
import os
import streamlit as st
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI
# ==========================================
# 1. Environment & Model Initialization
# ==========================================
load_dotenv()
# Initialize LLM instance
# Ensure your GOOGLE_API_KEY is defined inside the .env file
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash")
# ==========================================
# 2. Page Configuration & Header
# ==========================================
st.set_page_config(
page_title="AskBuddy AI QnA Bot",
page_icon="🤖",
layout="centered"
)
st.title("🤖 AskBuddy AI QnA Bot")
st.markdown("A lightweight conversational Q&A assistant powered by **LangChain**, **Google Gemini**, and **Streamlit**.")
# ==========================================
# 3. Session State History Initialization
# ==========================================
if "messages" not in st.session_state:
st.session_state.messages = []
# ==========================================
# 4. Render Prior Conversation History
# ==========================================
# On every script rerun, previous exchanges must be re-rendered to the UI
for message in st.session_state.messages:
role = message["role"]
content = message["content"]
with st.chat_message(role):
st.markdown(content)
# ==========================================
# 5. Handle Live User Input
# ==========================================
query = st.chat_input("Ask anything...")
if query:
# A. Display the User message immediately in the UI
with st.chat_message("user"):
st.markdown(query)
# B. Persist the User query into Session State
st.session_state.messages.append({
"role": "user",
"content": query
})
# C. Invoke the LangChain Google Gemini wrapper
response = llm.invoke(query)
ai_content = response.content
# D. Display the AI response immediately in the UI
with st.chat_message("ai"):
st.markdown(ai_content)
# E. Persist the AI response into Session State
st.session_state.messages.append({
"role": "ai",
"content": ai_content
})
8. Running the Application and Troubleshooting
8.1 Starting the Streamlit Server
Do not run Streamlit files using python filename.py. Doing so bypasses the Streamlit web server runtime and exits without launching the browser interface.
Start the app using the streamlit run CLI command:
streamlit run apps/1_qna_bot.py
Upon execution, the terminal provides two URLs:
- Local URL:
http://localhost:8501 - Network URL: An internally accessible IP address for devices on the same local network.
8.2 Common Technical Issues & Resolutions
9. Summary and Key Takeaways
- Decoupled Architecture: Using LangChain as an abstraction layer decouples the user interface (Streamlit) from the underlying LLM provider (Google Gemini). This makes it easy to swap models by changing only the class instantiation.
- Handling Model Output: LangChain's
.invoke()returns anAIMessageobject. The raw textual response must be extracted via.contentbefore sending it to UI elements. - Terminal Prototyping: A terminal-based
while Trueloop allows for quick debugging of prompt-response cycles before introducing web-layer complexities. - Streamlit's Reactive Execution: Streamlit re-runs scripts from top to bottom on every user interaction. Managing state across these runs requires explicit storage mechanisms.
- State Persistence:
st.session_stateacts as persistent in-memory storage on the Streamlit server, preserving conversation history across script reruns. - Dual-Phase Message Rendering: To prevent the UI from blinking or losing messages, the application must first render its historical log via an iteration over
st.session_state, and then immediately render active question-response pairs as they occur. - Chat Elements:
st.chat_inputandst.chat_messageprovide clean, built-in styling for conversational apps, automatically applying distinct visual styling foruserandairoles. - Server Deployment: Streamlit scripts must be launched via the
streamlit run <script_path>command to properly initialize the underlying Tornado web server and WebSocket transport.