NanoChat‑style ChatGPT Clone in Python

A beginner‑friendly mini‑LLM chat service using Flask and SQLite

Welcome to another official UVF IT guide! In this quick‑fire tutorial we’ll build a tiny ChatGPT‑style web app inspired by Karpathy’s nanochat, but stripped down to the absolute essentials so you can have a working clone in under an hour.

We’ll use Python, Flask for the API, and SQLite for persisting conversation history – all free‑tier, no external AI APIs required. By the end you’ll have a local server you can query with curl or a browser and see chat responses stored on disk.

Before you start…

Minimal NanoChat Clone Architecture
LOCAL STACK HTTP request SQL INSERT/SELECT Flask API Python + Flask SQLite DB Conversation store Browser / curl
1

Phase 1: Scaffold the project

Create a virtual environment and install Flask; set up the folder structure.

python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install Flask
mkdir -p nanochat
cd nanochat
cat > app.py <<'PY'
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route('/')
def health():
    return jsonify({'status':'ok'})

if __name__ == '__main__':
    app.run(debug=True)
PY
2

Phase 2: Define the data model (SQLite)

A single table stores each message with role (user/assistant) and timestamp.

app.py

import sqlite3, os
from datetime import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
DB_PATH = os.path.join(os.path.dirname(__file__), 'chat.db')

# ---------------------------------------------------------------------
# Database helpers – create table if missing and simple insert/select
# ---------------------------------------------------------------------

def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    with get_db() as db:
        db.execute('''
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                role TEXT NOT NULL,
                content TEXT NOT NULL,
                ts TEXT NOT NULL
            )
        ''')
        db.commit()

init_db()

@app.route('/chat', methods=['POST'])
def chat():
    data = request.get_json()
    user_msg = data.get('message', '')
    # store user message
    with get_db() as db:
        db.execute('INSERT INTO messages (role, content, ts) VALUES (?,?,?)',
                   ('user', user_msg, datetime.utcnow().isoformat()))
        db.commit()
    # placeholder assistant reply
    reply = 'I am a minimal clone; replace this with real LLM output.'
    with get_db() as db:
        db.execute('INSERT INTO messages (role, content, ts) VALUES (?,?,?)',
                   ('assistant', reply, datetime.utcnow().isoformat()))
        db.commit()
    return jsonify({'reply': reply})

@app.route('/')
def health():
    return jsonify({'status':'ok'})

if __name__ == '__main__':
    app.run(debug=True)
3

Phase 3: Build the chat endpoint (API/UI)

In this step we expose a Flask route that accepts a JSON payload containing the user's message. The endpoint stores the message in the SQLite database we created earlier and immediately returns a simple canned reply so you can see the whole request‑response loop in action. Keeping the reply static makes debugging easy while we later swap it for a real model. The code also returns a 200 status and a JSON body that the front‑end can consume.

app.py

import json
from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)
DB_PATH = 'chat.db'

def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, role TEXT, content TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)''')
    conn.commit()
    conn.close()

init_db()

@app.route('/chat', methods=['POST'])
def chat():
    data = request.get_json(force=True)
    user_msg = data.get('message', '')
    # Persist user message
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('INSERT INTO messages (role, content) VALUES (?, ?)', ('user', user_msg))
    conn.commit()
    conn.close()
    # Dummy assistant reply
    reply = "I am a placeholder assistant. Your message was: " + user_msg
    # Persist assistant reply
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('INSERT INTO messages (role, content) VALUES (?, ?)', ('assistant', reply))
    conn.commit()
    conn.close()
    return jsonify({'reply': reply})

if __name__ == '__main__':
    app.run(debug=True)
4

Phase 4: Run locally and test

Now that the endpoint is wired up, start the Flask development server. Flask will listen on http://127.0.0.1:5000 by default, and you can interact with the /chat route using curl, Postman, or a browser extension that sends JSON. Watching the console output will confirm that the request hits the server and that the SQLite inserts succeed.

export FLASK_APP=app.py
export FLASK_ENV=development
flask run
5

Phase 5: Verify and extend

After sending a request, open the SQLite file to make sure both the user message and the dummy reply are stored. You can also serve a very small HTML page that posts to /chat, giving you a quick UI without leaving the browser. This phase shows the full stack: front‑end → API → database → response.

sqlite3 chat.db "SELECT id, role, content FROM messages ORDER BY id;"

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsflask run → should show *Running on http://127.0.0.1:5000/*
POST request returns a replycurl -X POST -H "Content-Type: application/json" -d '{"message":"hello"}' http://127.0.0.1:5000/chat → JSON with 'reply' field
Message persisted in SQLitesqlite3 chat.db "SELECT role,content FROM messages WHERE role='user';" → should show 'hello'

What’s next?

You now have a functional local chat service. To move closer to the real nanochat, replace the placeholder reply with a call to a small open‑source model (e.g., GPT‑NeoX via HuggingFace) or expose the endpoint to the internet with ngrok for remote testing.