Migrate Chatbot from SQLite to PostgreSQL
Why Migrate from SQLite to PostgreSQL?
SQLite is excellent for local development, but:
-
It doesn’t persist in cloud environments like Render.
-
It’s not ideal for concurrent access.
-
PostgreSQL is a production-grade relational database.
By using PostgreSQL, your chatbot’s history and logic can persist between sessions and survive redeployments.
Migrate locally before deploying to Render!
Here’s a clear, step-by-step guide for setting up your local environment with PostgreSQL and SQLAlchemy, including all tools and Python packages you’ll need.
✅ 1. Install PostgreSQL Locally
On Windows:
-
Download PostgreSQL from the official site:
👉 https://www.postgresql.org/download/windows/ -
Run the installer and remember your password (you’ll use it later).
-
Make sure
psql
is added to your system PATH. -
Use pgAdmin (GUI tool included) or
psql
(CLI) to manage the database.
✅ 2. Create a Local Database
Open pgAdmin
or terminal and run:
CREATE DATABASE chatbot_db;
You’ll use this DB name in your environment variable.
✅ 3. Set Environment Variable Locally
You can put this in a .env
file in your project root:
DATABASE_URL=postgresql://username:password@localhost:5432/chatbot_db
Replace:
-
username
= your PostgreSQL user (default ispostgres
) -
password
= the password you set during installation -
5432
= default port -
chatbot_db
= the DB name you created
✅ 4. Install Required Python Packages
Run this in your project folder:
pip install sqlalchemy psycopg2 python-dotenv flask openai
If you also need tools for development:
pip install flask-cors debugpy
✅ 5. Your requirements.txt
(for both local & Render)
Create a requirements.txt
file (if you don’t already have it):
Flask
openai
SQLAlchemy
psycopg2
python-dotenv
flask-cors
debugpy
Code of project files:
To do the project code a clean code, I have created new file: db.py and moved functions: save_to_db, create_tables, get_last_message, get_conversation_history from chatbot.py to db.py and removed these functions from chatbot.py.
- File:db.py: This file contains database related code.
# db.py
# This code is designed to work with a PostgreSQL database using SQLAlchemy.
# seperating database from the main code with using postgresql database
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
import os
from dotenv import load_dotenv
import re
from models import Chat
from sqlalchemy import select, desc
from sqlalchemy.orm import Session
from models import Chat
from database import SessionLocal # <-- now comes from separate file
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
class Chat(Base):
__tablename__ = "chats"
id = Column(Integer, primary_key=True, index=True)
session_id = Column(String, index=True)
user_input = Column(Text)
bot_response = Column(Text)
timestamp = Column(DateTime, default=datetime.utcnow)
language = Column(String)
def create_tables():
Base.metadata.create_all(bind=engine)
def save_to_db(session_id, user_input, bot_response, language):
normalized_input = user_input.lower().translate(
str.maketrans('', '', re.escape('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'))
)
db = SessionLocal()
try:
new_chat = Chat(
session_id=session_id,
user_input=normalized_input,
bot_response=bot_response,
language=language,
timestamp=datetime.utcnow()
)
db.add(new_chat)
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
#Get conversation history for a specific session
def get_conversation_history(session_id):
with SessionLocal() as session:
history = session.query(Chat).filter_by(session_id=session_id).order_by(Chat.timestamp.asc()).all()
return [(chat.user_input, chat.bot_response, chat.timestamp, chat.language) for chat in history]
def get_last_message(user_input):
normalized_input = user_input.lower().translate(
str.maketrans('', '', r'''!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~''')
)
with SessionLocal() as session:
result = session.execute(
select(Chat.bot_response)
.where(Chat.user_input == normalized_input)
.order_by(desc(Chat.id))
.limit(1)
).fetchone()
return result[0] if result else None
2. File: database.py: This file contains Database relate imports
# database.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
3. File: .env: this file contains environment variables and only used in local development.
OPENAI_API_KEY: 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
DATABASE_URL=postgresql://username:password@localhost:5432/chatbot_db #this is for local development
4. File models.py: This file contains database table (chats) related code.
# models.py
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class Chat(Base):
__tablename__ = "chats"
id = Column(Integer, primary_key=True, index=True)
session_id = Column(String)
user_input = Column(Text)
bot_response = Column(Text)
timestamp = Column(DateTime, default=datetime.utcnow)
language = Column(String)
5. File: requirements.txt: this file needs both local and Deployment on Render
Flask
gunicorn
openai
SQLAlchemy
psycopg2
python-dotenv
flask-cors
debugpy
6. The main file: chatbot.py: this file is the main file for chatbot
# chatbot.py — AI Chatbot with language detection, datetime handling, and PostgreSQL (even SQLite) chat history
# This code is designed to work with a PostgreSQL database using SQLAlchemy.
# Seperating database from the main code with using PostgreSQL database
from flask import Flask, request, make_response, redirect, session
import uuid
import os
import json
import logging
import re
from datetime import datetime
from openai import OpenAI
from openai.types.chat import ChatCompletionMessage
from dotenv import load_dotenv
load_dotenv()
# Importing functions from db.py to use PostgreSQL database
from db import save_to_db, create_tables, get_last_message, get_conversation_history # Importing the save_to_db and create_tables, get_last_message functions from db.py to use pstgressql database
create_tables() # Ensure tables are created before running the app
# === Initialize Flask App ===
app = Flask(__name__)
# app.secret_key = "your_secret_key_here"
# === Enable Logging ===
logging.basicConfig(level=logging.DEBUG)
# === Initialize OpenAI Client ===
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# === Detect and Handle Simple Date/Time Questions ===
def detect_datetime_question(user_input):
text = user_input.lower()
now = datetime.now()
checks = {
("what time", "current time", "now", "time is it"): f"The current time is {now.strftime('%H:%M:%S')}.",
("what's the date", "what is the date", "current date", "today's date", "what date is it", "which date"): f"Today's date is {now.strftime('%Y-%m-%d')}.",
("which month", "what month", "current month", "month is this"): f"This month is {now.strftime('%B')}.",
("which day", "what day", "day is it", "current day", "today"): f"Today is {now.strftime('%A')}.",
("which year", "what year", "current year", "year is this"): f"This year is {now.strftime('%Y')}."
}
for keywords, answer in checks.items():
if any(kw in text for kw in keywords):
return answer
return None
# This function checks if the response from the OpenAI API is valid by looking for common error messages or keywords.
# This function checks if the response contains date/time-like content. It uses a list of keywords and regex patterns to identify such content.
import re
# This function checks if the response contains date/time-like content. It uses a list of keywords and regex patterns to identify such content.
def looks_like_datetime_response(response: str) -> bool:
response = response.lower()
# Regex: look for very specific datetime phrases or formats
datetime_patterns = [
r"\btoday is\b",
r"\bcurrent date\b",
r"\bthe date is\b",
r"\bnow is\b",
r"\btime is\b",
r"\b\d{4}-\d{2}-\d{2}\b", # e.g., 2025-05-17
r"\b\d{1,2} (january|february|march|april|may|june|july|august|september|october|november|december) \d{4}\b",
r"\bit's (monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
r"\bthis month is\b",
r"\bthe month is\b",
r"\btoday\b",
r"\bthis year\b",
r"\bthis week\b"
]
for pattern in datetime_patterns:
if re.search(pattern, response):
return True
return False
# === Validate AI Response ===
def is_valid_response(response: str) -> bool:
if not response or not response.strip():
return False
invalid_keywords = ["something went wrong", "error:", "exception", "traceback",
"insufficient_quota", "no longer supported", "invalid request",
"you exceeded your quota"]
return not any(err in response.lower() for err in invalid_keywords)
# === Chatbot Main Logic ===
def chatbot_response(session_id, user_input):
normalized_input = user_input.lower().translate(str.maketrans('', '', re.escape('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~')))
# Handle datetime Qs
datetime_answer = detect_datetime_question(user_input)
if datetime_answer:
return datetime_answer
# Return cached reply
cached = get_last_message(normalized_input)
if cached:
return cached
# Query OpenAI
messages = [
{
"role": "system",
"content": (
"You are a helpful assistant. detect the language of the user input. "
"respond to the input in the same language. "
"Return the result in the following JSON format:\n\n"
"{\n"
' "language": "<language_code>",\n'
' "response": "<your response in the same language>"\n'
"}\n\n"
"Use ISO 639-1 language codes (e.g., 'en', 'sv', 'tr', 'fa')."
)
},
{
"role": "user",
"content": user_input
}
]
try: # Send request to OpenAI
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages,
temperature=0.7
)
reply = completion.choices[0].message.content
data = json.loads(reply)
response = data.get("response", "").strip()
language = data.get("language", "").lower().strip()
except Exception as e:
logging.error(f"Failed to parse OpenAI response: {e}")
response = reply.strip() if 'reply' in locals() else "Sorry, I don't have an answer right now."
language = None
if is_valid_response(response) \
and not detect_datetime_question(user_input) \
and not looks_like_datetime_response(response):
save_to_db(session_id, normalized_input, response, language)
return response
# === Home Page ===
@app.route("/", methods=["GET"])
def home():
session_id = request.cookies.get("session_id") or str(uuid.uuid4())
html = """
<html><head><style>
body { font-family: Arial; max-width: 600px; margin: auto; padding: 20px; }
input[type="text"] { width: 80%; padding: 10px; }
button { padding: 10px 15px; margin-top: 10px; }
</style></head><body>
<h2>Chatbot</h2>
<form action="/chat" method="post">
<input type="text" name="message" placeholder="Type your message" required>
<button type="submit">Send</button>
</form>
<form action="/history" method="get">
<button type="submit">Show History</button>
</form>
</body></html>
"""
response = make_response(html)
response.set_cookie("session_id", session_id, max_age=60 * 60 * 24 * 7)
return response
# === Chat Handler ===
@app.route("/chat", methods=["POST"])
def chat():
session_id = request.cookies.get("session_id") or str(uuid.uuid4())
user_input = request.form.get("message", "")
if not user_input:
return redirect("/")
bot_reply = chatbot_response(session_id, user_input)
html = f"""
<html><head><style>
body {{ font-family: Arial; max-width: 600px; margin: auto; padding: 20px; }}
.user {{ color: #0066cc; font-weight: bold; }}
.bot {{ color: #009933; }}
.chat-box {{ border-bottom: 1px solid #ccc; padding: 10px 0; }}
button {{ padding: 10px 15px; margin-top: 10px; }}
</style></head><body>
<h2>Chatbot</h2>
<div class="chat-box">
<div class="user">You: {user_input}</div>
<div class="bot">Bot: {bot_reply}</div>
</div>
<form action="/" method="get"><button type="submit">New Question</button></form>
<form action="/history" method="get"><button type="submit">Show History</button></form>
</body></html>
"""
response = make_response(html)
response.set_cookie("session_id", session_id, max_age=60 * 60 * 24 * 7)
return response
# === History Page ===
@app.route("/history", methods=["GET"])
def history():
session_id = request.cookies.get("session_id")
if not session_id:
return redirect("/")
history = get_conversation_history(session_id)
history_html = ""
for user_input, bot_response, timestamp, language in history:
lang_display = language.upper() if language else "??"
history_html += f"""
<div class="chat-box">
<div><strong>[{timestamp}] [{lang_display}]</strong></div>
<div class="user">You: {user_input}</div>
<div class="bot">Bot: {bot_response}</div>
</div>
"""
html = f"""
<html><head><style>
body {{ font-family: Arial; max-width: 600px; margin: auto; padding: 20px; }}
.user {{ color: #0066cc; font-weight: bold; }}
.bot {{ color: #009933; }}
.chat-box {{ border-bottom: 1px solid #ccc; padding: 10px 0; }}
button {{ padding: 10px 15px; margin-top: 10px; }}
</style></head><body>
<h2>Chat History</h2>
{history_html}
<form action="/" method="get"><button type="submit">New Question</button></form>
</body></html>
"""
return html
# === Run Server with Debug Support ===
if __name__ == "__main__":
import debugpy
debugpy.listen(("0.0.0.0", 5678))
print("Waiting for debugger connection...")
create_tables() # Ensure tables are created before running the app
app.run(debug=True, use_reloader=False, use_debugger=False)
✅ 7. Run Your App Locally
In terminal:
python chatbot.py
Ensure this line is in your main block to create the tables:
create_tables()
As follow:
<strong>create_tables()</strong> # Ensure tables are created before running the app
app.run(debug=True, use_reloader=False, use_debugger=False)
Troubleshooting Tips
-
If you get an error like
psycopg2 module not found
, try installing the binary version:
pip install psycopg2-binary
- If
.env
variables aren’t loading, make sure you calledload_dotenv()
at the top of your script. - Donte forget to add:PostgreSQL (C:\Program Files\PostgreSQL\17\bin) in your path.
✅ Done!
Now your chatbot is connected to PostgreSQL and ready to test locally. Once it works, deploying to Render will be smooth because you’ll already be using DATABASE_URL
.
2. PostgreSQL configuration on Render
Migrating a Python Chatbot from SQLite to PostgreSQL for Persistent Deployment on Render. In this part of post, you’ll migrate a Python Flask chatbot from SQLite to PostgreSQL, using SQLAlchemy and environment variables for secure and scalable deployment.
Steps to Deploy Chatbot with PostgreSQL on Render
Step 1. Add PostgreSQL Database in Render
-
Go to your Render dashboard
-
Click “Add New” → “PostgreSQL”
-
Give your database a name like
chatbot_db
-
Choose the free tier unless you need more storage/performance
-
After it’s created, go to the database page → click “Connection” (Connect)
-
Copy the External Database URL (it looks like
postgres://user:pass@host:port/dbname
) for me: (postgresql://chatbot_db_z3f8_user:IkQGqICGvFKHIhNHS0aikxM4wr9JDhOW@dpg-d0ud97mmcj7s739inqrg-a.frankfurt-postgres.render.com/chatbot_db_z3f8)
Step 2. Update Environment Variables
Go to your chatbot Web Service on Render:
-
Click on your service → Settings → Environment
-
Add or update the following variable:
Key: DATABASE_URL
Value: <paste your PostgreSQL connection string here>
connection string thatyou have copied in the step 1.
Make sure DATABASE_URL
matches the variable name used in your db.py
or settings
.
💡 Also ensure other env vars like OPENAI_API_KEY
are still there.
like as following:
Step 3: Push Latest Code to Git
Make sure your changes (migrated to PostgreSQL) are committed and pushed to the repo connected to Render:
git add .
git commit -m "Migrate chatbot to PostgreSQL"
git push
Step 4: Verify: Create Table on First Run
If you’re calling create_tables()
in if __name__ == "__main__"
block, Render will run it on deployment.
Make sure this is in your chatbot.py
:
if __name__ == "__main__":
create_tables() # Creates the chats table if it doesn't exist
app.run(debug=True, use_reloader=False, use_debugger=False)
Step 5: Check Logs
If anything fails:
-
Go to Render dashboard → your service → Logs
-
Look for errors like:
-
psycopg2.OperationalError
-
relation "chats" does not exist
(means table creation logic failed) -
.env parsing errors
-
Step 6: Test Deployment
Once live:
-
Open your Render app URL (e.g.
https://yourchatbot.onrender.com
) -
Send some messages to the chatbot
-
Refresh page or come back later — history should persist ✅
Step 7: (Optional) Disable SQLite Completely
You can now safely delete:
-
Old
chatbot.db
file (local) -
Any old
sqlite3.connect()
code if still present
Pro Tip: Hide Secret Info
Make sure .env
is in .gitignore
:
Pro Tip: Hide Secret Info
Make sure .env is in .gitignore:
Summary of What You Configured for PostgreSQL on Render:
Step | What You Did |
---|---|
✅ 1 | Created a PostgreSQL database |
✅ 2 | Added DATABASE_URL as env variable |
✅ 3 | Switched from SQLite to SQLAlchemy with PostgreSQL |
✅ 4 | Deployed to Render and tested |
✅ 5 | Verified data is persistent via PostgreSQL |
In this post we have gone through Migration of Chatbot from SQLite to PostgreSQL both in development (local) and deployment on Render.
The next post is: Add Voice functionality to chatbot
This post is part of AI (Artificial Intelligence) step by step