Context-Engineered AI Assistant

Build a local AI chatbot that uses context engineering to give smarter answers.

Introduction

30 Second Summary

Ever noticed how AI chatbots forget what you said two messages ago? Or give vague, generic answers to questions they should know inside out? The difference between a useless chatbot and a helpful one comes down to what information the AI can see when it generates each response.

In this project, you will build a local AI assistant web app and engineer the context pipeline that transforms it from a forgetful, generic chatbot into one that remembers your conversation and answers domain questions accurately.

What You'll Build

You will chat with a local AI model through your browser, ask it a question about context engineering, and watch it give a detailed, accurate answer pulled from your own knowledge base. Then you will click "Compare" and see the same question answered without context engineering side by side, proving the difference your engineering makes.

By the end of this project, you'll have:

  • A working chat interface in your browser where you type messages and see your local AI respond in real-time, remembering everything you said earlier in the conversation.
  • A context engine that assembles system instructions, retrieves relevant knowledge documents, and loads conversation history before each AI call, visibly improving response quality.
  • A side-by-side comparison view where you ask the same question and see the AI's answer with and without context engineering, proving the difference in a single screen.
  • Secret Mission: A Context Inspector panel that reveals exactly what tokens the model receives on each turn, making the invisible engineering visible.

Are there any prerequisites?

None. This guide includes setup for all required tools. You will install everything from scratch in Step 1, including Flask and the Ollama Python library.

Before We Start

You are about to build a local AI assistant that proves a powerful idea: the same AI model gives dramatically better answers when you engineer what information it sees.

To make this happen, you need a web server to handle chat requests and a Python library to talk to your local Ollama model. In this step, you will create your project folder, set up a virtual environment, and install the two libraries that power everything.

In this step, get ready to:
  • Create a project folder and virtual environment.
  • Install Flask and the ollama Python library.
  • Verify Ollama is running and deepseek-r1:8b responds.
Create a project folder and virtual environment

A virtual environment keeps this project's packages separate from everything else on your machine. It means you can install Flask and the ollama library here without affecting other Python projects.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir context-assistant
cd context-assistant
  • Create a virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment by running the command for your operating system:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activate command for your operating system. On macOS/Linux it starts with source. On Windows it uses the Scripts\activate path.

If you see a "permission denied" error on macOS, try chmod +x venv/bin/activate first, then run the source command again.

Help me activate my virtual environment.

Install Flask and the ollama Python library

Flask is a lightweight web framework that will serve your chat interface. The ollama library lets Python send messages to your local AI model and get responses back.

  • Install both packages by running:
pip install Flask ollama

You should see output showing both packages downloading and installing successfully.

What does this command do?

  • pip install downloads and installs Python packages from the Python Package Index (PyPI).
  • Flask is the web framework that handles HTTP requests and serves HTML pages.
  • ollama is the Python library that communicates with your local Ollama server to send messages to AI models.

Seeing a "pip not found" error?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, run the activate command from the previous substep again.

If pip still is not found, try pip3 install Flask ollama instead.

Help me fix my pip installation error.

Verify Ollama is running and deepseek-r1:8b responds

Before writing any code, you need to confirm that Ollama is running on your machine and that the deepseek-r1:8b model is available.

  • Check which models are available by running:
ollama ls

You should see deepseek-r1:8b listed in the output table.

Don't see deepseek-r1:8b in the list?

If the command shows no models or deepseek-r1:8b is missing, pull the model by running ollama pull deepseek-r1:8b in your terminal. This download is approximately 5 GB and may take a few minutes.

If you see "command not found", Ollama is not installed. Visit ollama.com to download and install it first.

Help me get Ollama and deepseek-r1:8b working.

Now, before you run the final verification, what do you think will happen if you try to import Flask and the ollama library in a single Python command?

  • Verify everything is working together by running:
python -c "from flask import Flask; from ollama import chat; print('Ready!')"

You should see Ready! printed in your terminal. This confirms that both libraries are installed correctly and your environment is ready to go.

Seeing an import error?

If you see ModuleNotFoundError, the packages did not install into your active virtual environment. Run which python (macOS) or where python (Windows) and confirm the path includes venv. If it does not, deactivate, then reactivate the virtual environment and reinstall.

Help me fix my import error.

Your environment is ready. Next up, you will build a working chatbot and see exactly where it falls short without memory or context.

Build the Naive Chatbot

Your tools are installed and Ollama is ready to respond. Now you'll build the actual chatbot that connects everything together.

The plan is deliberate. You'll build a chatbot that works but has a critical flaw: it forgets everything you say the moment you say it. This frustration is the whole point. Experiencing the memory gap firsthand makes it obvious why context engineering matters.

In this step, get ready to:
  • Create a Flask app that sends messages to deepseek-r1:8b and returns responses.
  • Build an HTML chat interface where you type messages and see replies.
  • Test the chatbot and discover its memory limitation.
Create the Flask app with a chat route

A Flask app needs routes that tell it how to respond to different URLs. You'll create two: one to serve the chat page, and one to handle incoming messages by sending them to deepseek-r1:8b.

  • Create a file called app.py in your project folder by pasting the following code:
from flask import Flask, render_template, request, jsonify
from ollama import chat

app = Flask(__name__)

# Serve the chat page
@app.route("/")
def index():
    return render_template("index.html")

# Handle incoming chat messages
@app.route("/chat", methods=["POST"])
def chat_route():
    data = request.get_json()
    user_message = data["message"]
    # Send only the current message with no history
    messages = [{"role": "user", "content": user_message}]
    response = chat(model="deepseek-r1:8b", messages=messages)
    return jsonify({"response": response.message.content})

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

What does this code do?

  • The / route serves an HTML page (your chat interface) when someone visits the app in their browser.
  • The /chat route receives a message from the user, puts it in a list with the role user, sends that list to deepseek-r1:8b, and returns the AI response as JSON.
  • The messages list only ever contains the single current message. Nothing from previous turns is included. This is the deliberate gap you'll feel later.
  • Save app.py.
  • Confirm the file exists by running:
ls

You should see app.py listed in the output.

Don't see app.py?

  • Make sure you saved the file inside your project folder, not somewhere else on your system.
  • Check that your terminal is inside the project folder. Run pwd to confirm.
  • help me find where my app.py was saved
Build the chat interface

Flask looks for HTML files inside a folder called templates. You need to create that folder and add the chat page inside it.

  • Create the templates folder by running:
mkdir templates
  • Inside the templates folder, create a file called index.html and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Naive Chatbot</title>
</head>
<body>
    <div id="chat-container"></div>
    <input type="text" id="user-input" placeholder="Type a message...">
    <button onclick="sendMessage()">Send</button>
    <script>
        async function sendMessage() {
            const message = document.getElementById('user-input').value.trim();
            if (!message) return;
            document.getElementById('user-input').value = '';
            // Send the message to the Flask backend
            const res = await fetch('/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({message: message})
            });
            // Display the AI response on the page
            const data = await res.json();
            const div = document.createElement('div');
            div.textContent = data.response;
            document.getElementById('chat-container').appendChild(div);
        }
    </script>
</body>
</html>

What does this code do?

  • The chat-container div is where AI responses will appear on the page.
  • The sendMessage() function grabs whatever the user typed, sends it to your /chat route, and adds the AI response as a new div element.
  • The input field clears itself after each send so the user can type the next message immediately.
  • Save templates/index.html.

✔️ Awesome, I've got everything!

Great. Make sure both app.py and templates/index.html are saved.

ⓧ I'd like to double check the full code

Here is app.py:

from flask import Flask, render_template, request, jsonify
from ollama import chat

app = Flask(__name__)

# Serve the chat page
@app.route("/")
def index():
    return render_template("index.html")

# Handle incoming chat messages
@app.route("/chat", methods=["POST"])
def chat_route():
    data = request.get_json()
    user_message = data["message"]
    # Send only the current message with no history
    messages = [{"role": "user", "content": user_message}]
    response = chat(model="deepseek-r1:8b", messages=messages)
    return jsonify({"response": response.message.content})

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

Here is templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Naive Chatbot</title>
</head>
<body>
    <div id="chat-container"></div>
    <input type="text" id="user-input" placeholder="Type a message...">
    <button onclick="sendMessage()">Send</button>
    <script>
        async function sendMessage() {
            const message = document.getElementById('user-input').value.trim();
            if (!message) return;
            document.getElementById('user-input').value = '';
            // Send the message to the Flask backend
            const res = await fetch('/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({message: message})
            });
            // Display the AI response on the page
            const data = await res.json();
            const div = document.createElement('div');
            div.textContent = data.response;
            document.getElementById('chat-container').appendChild(div);
        }
    </script>
</body>
</html>
Run the app and test its memory

Both files are ready. Time to start the server and have a conversation with your AI.

  • Start the Flask development server by running:
python app.py

What should I see?

Flask prints several lines including Running on http://127.0.0.1:5000. This means the server is active and waiting for requests. Your terminal stays occupied while the server runs.

  • Open your browser and go to http://127.0.0.1:5000.
  • Type a message like Hello, what can you do? in the input field and click Send.

After a few seconds, you should see the AI respond directly on the page. It works.

Not seeing a response?

  • Check your terminal for error messages. A TemplateNotFound error means index.html is not inside the templates folder.
  • Make sure Ollama is still running in a separate terminal. If it stopped, start it again before retrying.
  • If the page loads but nothing happens after clicking Send, open your browser's developer console (F12) and check for network errors.
  • help me debug my Flask chatbot not responding

Now for the real test. Before you type the next message, predict what will happen: do you think the AI remembers what you just said?

  • Type What did I just say? and click Send.

The AI has no idea. It responds as if this is the first message it has ever received.

Why does it forget everything?

Look at the messages list in your chat_route function. It only ever contains one item: the current message. Every time you send something, the AI sees a single message with zero history.

This is not a flaw in the model. deepseek-r1:8b is perfectly capable of multi-turn conversation. The problem is what you are feeding it. You are engineering zero context.

You now have proof of the problem. The model is smart, but your app feeds it nothing useful. Next, you'll fix this by designing a database that stores every message and gives the AI a real memory.

Design the Database and Add Memory

You proved in the last step that the chatbot works. You also proved it forgets everything the moment you send the next message.

The reason is simple: nothing is stored. Each request builds a fresh messages array with only the current message in it. In this step, you will design a SQLite database with proper relationships (conversations have many messages), store every exchange, and feed conversation history back into the AI on each turn.

In this step, get ready to:
  • Design and create a SQLite schema with conversations and messages tables linked by a foreign key.
  • Update the Flask app to store every message and load conversation history before each AI call.
  • Test multi-turn memory by asking the AI to recall earlier messages.
Design and create the SQLite schema

A relational database schema defines how your data is organized into tables and how those tables relate to each other. You need two tables: one for conversations and one for messages. Each message belongs to a conversation through a foreign key.

  • Create a new file called database.py in your project folder (the same folder as app.py) and add the following connection and schema code:
import sqlite3

DATABASE = "assistant.db"

def get_db():
    db = sqlite3.connect(DATABASE)
    db.row_factory = sqlite3.Row
    return db

def init_db():
    db = get_db()
    db.executescript("""
        CREATE TABLE IF NOT EXISTS conversations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT DEFAULT 'New Conversation',
            created_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            conversation_id INTEGER NOT NULL,
            role TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TEXT DEFAULT (datetime('now')),
            FOREIGN KEY (conversation_id) REFERENCES conversations(id)
        );
    """)
    db.commit()
    db.close()

What does this code do?

  • DATABASE = "assistant.db" sets the filename for your SQLite database. SQLite stores everything in a single local file.
  • get_db() opens a connection to the database. The row_factory = sqlite3.Row setting lets you access columns by name instead of index.
  • init_db() creates the two tables if they do not already exist. The conversations table holds a title and timestamp. The messages table stores each message with its role (user or assistant) and links back to its conversation through conversation_id.
  • FOREIGN KEY (conversation_id) REFERENCES conversations(id) enforces the relationship: every message must belong to a valid conversation.
  • Save database.py.
  • Verify the schema works by running this command in your terminal:
python -c "from database import init_db; init_db(); print('Database created!')"
  • You should see Database created! printed. Confirm the database file exists by running:
ls
  • You should see assistant.db in the output alongside app.py and your other project files.

Seeing ModuleNotFoundError?

  • Make sure you are running the command from inside your project folder (the same folder that contains database.py).
  • Check that your virtual environment is activated. You should see (venv) at the start of your terminal prompt.

Why can't Python find my database module?

  • Now add the helper functions below init_db() in database.py:
def create_conversation(db):
    cursor = db.execute("INSERT INTO conversations (title) VALUES (?)", ("New Conversation",))
    db.commit()
    return {"id": cursor.lastrowid}

def store_message(db, conversation_id, role, content):
    cursor = db.execute(
        "INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)",
        (conversation_id, role, content)
    )
    db.commit()
    return cursor.lastrowid

def get_conversation_messages(db, conversation_id, limit=10):
    cursor = db.execute(
        "SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY created_at ASC LIMIT ?",
        (conversation_id, limit)
    )
    return [{"role": row["role"], "content": row["content"]} for row in cursor.fetchall()]

What do these helper functions do?

  • create_conversation() inserts a new row into the conversations table and returns its id. Every chat session gets its own conversation.
  • store_message() saves a single message (with its role and content) linked to a specific conversation.
  • get_conversation_messages() retrieves the last 10 messages for a conversation, ordered oldest first. This is what you will feed back to the AI as history.
  • Save database.py.
  • Verify the helper functions work by running this quick test:
python -c "from database import get_db, create_conversation, store_message, get_conversation_messages; db = get_db(); c = create_conversation(db); store_message(db, c['id'], 'user', 'hello'); print(get_conversation_messages(db, c['id']))"
  • You should see a list containing one dictionary: [{'role': 'user', 'content': 'hello'}]. This confirms your database can store and retrieve messages.

Seeing an OperationalError?

  • If you see "no such table: conversations", the init_db() step may not have run successfully. Delete assistant.db and re-run the init command from earlier.
  • Check for typos in your SQL, especially the FOREIGN KEY line. The column name conversation_id must match exactly.

Help me debug my database setup.

✔️ Awesome, I've got everything!

Great. Make sure you have saved database.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete database.py file. Make sure yours matches:

import sqlite3

DATABASE = "assistant.db"

def get_db():
    db = sqlite3.connect(DATABASE)
    db.row_factory = sqlite3.Row
    return db

def init_db():
    db = get_db()
    db.executescript("""
        CREATE TABLE IF NOT EXISTS conversations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT DEFAULT 'New Conversation',
            created_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            conversation_id INTEGER NOT NULL,
            role TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TEXT DEFAULT (datetime('now')),
            FOREIGN KEY (conversation_id) REFERENCES conversations(id)
        );
    """)
    db.commit()
    db.close()
def create_conversation(db):
    cursor = db.execute("INSERT INTO conversations (title) VALUES (?)", ("New Conversation",))
    db.commit()
    return {"id": cursor.lastrowid}

def store_message(db, conversation_id, role, content):
    cursor = db.execute(
        "INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)",
        (conversation_id, role, content)
    )
    db.commit()
    return cursor.lastrowid

def get_conversation_messages(db, conversation_id, limit=10):
    cursor = db.execute(
        "SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY created_at ASC LIMIT ?",
        (conversation_id, limit)
    )
    return [{"role": row["role"], "content": row["content"]} for row in cursor.fetchall()]
Update the Flask app to use the database

Your database module is ready. Now you need to wire it into the Flask app so that every message is stored and the AI receives full conversation history on each turn.

  • Open app.py and replace its entire contents with the following. Start with the imports, initialization, and index route:
import sqlite3
from flask import Flask, render_template, request, jsonify
from database import init_db, get_db, store_message, get_conversation_messages, create_conversation
from ollama import chat

app = Flask(__name__)

init_db()

@app.route("/")
def index():
    db = get_db()
    conversation = create_conversation(db)
    return render_template("index.html", conversation_id=conversation["id"])
  • Continue adding the chat route and main block below the index route:
@app.route("/chat", methods=["POST"])
def chat_route():
    data = request.get_json()
    user_message = data["message"]
    conversation_id = data["conversation_id"]
    db = get_db()
    store_message(db, conversation_id, "user", user_message)
    history = get_conversation_messages(db, conversation_id, limit=10)
    messages = [{"role": m["role"], "content": m["content"]} for m in history]
    response = chat(model="deepseek-r1:8b", messages=messages)
    assistant_content = response.message.content
    store_message(db, conversation_id, "assistant", assistant_content)
    return jsonify({"response": assistant_content})

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

What changed in app.py?

  • init_db() runs at startup to ensure the tables exist before any request comes in.
  • The index route now creates a fresh conversation and passes its id to the template. Each browser tab gets its own conversation.
  • The chat route stores the user message first, then loads the full conversation history from the database, sends that entire history array to the AI, and finally stores the assistant response too.
  • The AI now sees every previous message in the conversation, not just the current one. This is what gives it memory.
  • Save app.py.
  • Now open templates/index.html and replace its entire contents with the following:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chatbot with Memory</title>
</head>
<body>
    <div id="chat-container"></div>
    <input type="text" id="user-input" placeholder="Type a message...">
    <button onclick="sendMessage()">Send</button>
    <script>
        const conversationId = {{ conversation_id }};
        async function sendMessage() {
            const message = document.getElementById('user-input').value.trim();
            if (!message) return;
            document.getElementById('user-input').value = '';
            const res = await fetch('/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({message: message, conversation_id: conversationId})
            });
            const data = await res.json();
            const div = document.createElement('div');
            div.textContent = data.response;
            document.getElementById('chat-container').appendChild(div);
        }
    </script>
</body>
</html>

What changed in index.html?

  • const conversationId = {{ conversation_id }} captures the conversation id that Flask passed into the template. The double braces are Jinja2 template syntax.
  • The fetch body now includes conversation_id alongside the message. This tells the server which conversation to load history from and save new messages to.
  • Save templates/index.html.

Not sure about the file structure?

Your project folder should now contain: app.py, database.py, assistant.db, and a templates/ folder with index.html inside it. If you are missing any of these, double check you created them in the correct location.

Help me check my project file structure.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before testing.

ⓧ I'd like to double check the full code

Here is the complete app.py:

import sqlite3
from flask import Flask, render_template, request, jsonify
from database import init_db, get_db, store_message, get_conversation_messages, create_conversation
from ollama import chat

app = Flask(__name__)

init_db()

@app.route("/")
def index():
    db = get_db()
    conversation = create_conversation(db)
    return render_template("index.html", conversation_id=conversation["id"])
@app.route("/chat", methods=["POST"])
def chat_route():
    data = request.get_json()
    user_message = data["message"]
    conversation_id = data["conversation_id"]
    db = get_db()
    store_message(db, conversation_id, "user", user_message)
    history = get_conversation_messages(db, conversation_id, limit=10)
    messages = [{"role": m["role"], "content": m["content"]} for m in history]
    response = chat(model="deepseek-r1:8b", messages=messages)
    assistant_content = response.message.content
    store_message(db, conversation_id, "assistant", assistant_content)
    return jsonify({"response": assistant_content})

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

And here is the complete templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chatbot with Memory</title>
</head>
<body>
    <div id="chat-container"></div>
    <input type="text" id="user-input" placeholder="Type a message...">
    <button onclick="sendMessage()">Send</button>
    <script>
        const conversationId = {{ conversation_id }};
        async function sendMessage() {
            const message = document.getElementById('user-input').value.trim();
            if (!message) return;
            document.getElementById('user-input').value = '';
            const res = await fetch('/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({message: message, conversation_id: conversationId})
            });
            const data = await res.json();
            const div = document.createElement('div');
            div.textContent = data.response;
            document.getElementById('chat-container').appendChild(div);
        }
    </script>
</body>
</html>
Test the memory with a multi-turn conversation

Before you had a database, the AI forgot everything between messages. Now it should remember the full conversation. Time to prove it.

  • First, delete the old database file so you start fresh. Run this in your terminal:
rm assistant.db
  • Start the Flask server by running:
python app.py
  • Open http://127.0.0.1:5000 in your browser.
  • Send at least 3 messages in a row. For example:
  • Message 1: My name is Alex
  • Message 2: I'm learning about databases
  • Message 3: What is my name and what am I learning about?

Before you send that third message, what do you think the AI will say? Will it remember your name and topic, or forget like before?

The AI should correctly recall both your name and what you said you are learning about. It has memory now because the full conversation history is loaded from the database and sent along with each new message.

Why does this work now?

In Step 2, the messages array only ever had one item: the current message. The AI had no way to know what came before.

Now, get_conversation_messages loads the last 10 messages from the database. The AI receives the full thread on every turn, so it can reference anything said earlier in the conversation.

AI still not remembering?

  • Check your browser's developer console (F12) for errors. If you see a 500 error, your terminal running Flask will show the traceback.
  • Make sure index.html includes const conversationId = {{ conversation_id }} on the line before the sendMessage function. Without it, the server does not know which conversation to load.
  • Confirm you deleted the old assistant.db and restarted the server. A stale database from your earlier test might cause issues.

Why is my chatbot still forgetting messages?

Your chatbot now has persistent memory within a conversation. It can reference anything said in the last 10 messages. Next up, you will build the context engine that goes beyond memory. It will assemble system instructions and pull in relevant knowledge documents to make the AI dramatically smarter.

Build the Context Engine

Your chatbot can remember previous messages now, but try asking it a domain-specific question like "what is context engineering?" and you will get a generic, vague answer. The AI has no specialized knowledge to draw from.

The context engine fixes this. It assembles system instructions, selects relevant knowledge from a database, and loads conversation history into a structured context window before every AI call. This is the core of context engineering.

In this step, get ready to:
  • Add a knowledge_documents table and a seed script that populates it with domain knowledge.
  • Build a context engine module that assembles system instructions, retrieved knowledge, and conversation history.
  • Wire the context engine into your chat route and add a /compare endpoint to see the difference.
Add a knowledge table and seed script

The AI needs a library of documents it can search for relevant information. You will add two new tables to your database and two helper functions that the context engine will use.

  • In VS Code, open database.py.
  • Add this import at the top of the file, directly below the existing import sqlite3 line:
from datetime import datetime
  • Find the init_db function. Inside the db.executescript call, find the closing ); for the messages table.
  • Add these two new table definitions directly below the messages table, still inside the triple-quoted SQL string:
        CREATE TABLE IF NOT EXISTS knowledge_documents (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            category TEXT NOT NULL,
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS context_logs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            message_id INTEGER NOT NULL,
            context_assembled TEXT NOT NULL,
            token_estimate INTEGER NOT NULL,
            created_at TEXT DEFAULT (datetime('now')),
            FOREIGN KEY (message_id) REFERENCES messages(id)
        );

What do these tables store?

The knowledge_documents table holds your AI's reference library. Each document has a title, content, and category.

The context_logs table records exactly what the AI sees on each turn. This creates observability into your context engineering pipeline. The foreign key on message_id links each log back to the message that triggered it.

  • Now scroll to the bottom of database.py and add this function below get_conversation_messages:
def get_relevant_knowledge(db, query):
    # Split the query into individual words for matching
    words = query.lower().split()
    cursor = db.execute("SELECT title, content FROM knowledge_documents")
    results = []
    for row in cursor.fetchall():
        # Score each document by how many query words appear in it
        text = (row["title"] + " " + row["content"]).lower()
        score = sum(1 for word in words if word in text)
        if score > 0:
            results.append({"title": row["title"], "content": row["content"], "score": score})
    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:3]

What does this function do?

This is a simple keyword-matching retrieval function. It splits the user's query into words, then scores each document by counting how many of those words appear in it. The top 3 matches are returned.

Production systems use vector embeddings for semantic search, but keyword matching teaches the same retrieval concept without extra infrastructure.

  • Add one more function directly below get_relevant_knowledge:
def store_context_log(db, message_id, context_assembled, token_estimate):
    # Record what the AI saw for observability
    db.execute(
        "INSERT INTO context_logs (message_id, context_assembled, token_estimate) VALUES (?, ?, ?)",
        (message_id, context_assembled, token_estimate)
    )
    db.commit()

Why log the context?

You cannot improve what you cannot see. Logging the assembled context for each message lets you inspect exactly what tokens the model received, which is essential for debugging and improving your context engineering.

  • Save database.py.

Now you need documents for the AI to retrieve. You will create a seed script that populates the knowledge base.

  • Create a new file called seed_knowledge.py in your project folder.
  • Paste the following code to set up the script and define the first two knowledge documents:
from database import get_db, init_db

init_db()
db = get_db()

documents = [
    {
        "title": "What is Context Engineering",
        "content": "Context engineering is the practice of designing what information an AI model sees on every inference call. It includes system prompts, user input, retrieved documents, conversation history, tool definitions, and long-term memory. Unlike prompt engineering which focuses on phrasing, context engineering designs the entire pipeline that produces the context. The four pillars are: Instructions (system prompts and behavioral framing), Retrieval (RAG and grounded search), Memory (short-term conversation and long-term persistent state), and Available Tools (function-calling surface). Context engineering treats context as a finite, precious resource where the goal is to find the smallest set of high-signal tokens that maximize the desired outcome.",
        "category": "ai-engineering"
    },
    {
        "title": "What is Harness Engineering",
        "content": "Harness engineering is the discipline of building everything around an AI model that the model cannot do on its own: tools, state, execution, context, permissions, and verification. The formula is Agent = Model + Harness. A well-built harness includes Guides (feedforward controls that steer the agent before it acts), Sensors (feedback controls that observe and help self-correct), Computational controls (deterministic tooling like tests and linters), and Inferential controls (semantic evaluation using LLM-as-judge). The harness turns raw model intelligence into reliable, autonomous action. Coined by Mitchell Hashimoto in February 2026 and popularized by Martin Fowler and OpenAI.",
        "category": "ai-engineering"
    },
  • Continue directly below with the remaining two documents and the insertion loop:
    {
        "title": "Context Engineering vs Prompt Engineering",
        "content": "Prompt engineering focuses on how to phrase instructions for the model. Context engineering focuses on what information the model has access to when it generates responses. For single-turn chats they nearly overlap, but for multi-step AI agents running in loops, context engineering is the dominant challenge. Evidence from Shopify (2025) showed that improving prompts without changing context moved accuracy from 71% to 74%, but restructuring context without changing the prompt jumped accuracy from 71% to 93%. The key operations are: Write (save context outside the window), Select (retrieve only relevant context), Compress (summarize to retain only decision-worthy tokens), and Isolate (restrict data visibility between sub-agents).",
        "category": "ai-engineering"
    },
    {
        "title": "Database Schema Design Basics",
        "content": "A relational database schema defines how data is organized into tables and how tables relate to each other. Key concepts: Primary keys uniquely identify each row. Foreign keys create relationships between tables (e.g., a message belongs to a conversation). One-to-many relationships are the most common pattern (one conversation has many messages). Schema design decisions include choosing data types, setting constraints (NOT NULL, UNIQUE), and deciding which fields to index for performance. Good schema design separates concerns: each table represents one entity, and relationships are explicit through foreign keys.",
        "category": "database"
    }
]

for doc in documents:
    db.execute(
        "INSERT INTO knowledge_documents (title, content, category) VALUES (?, ?, ?)",
        (doc["title"], doc["content"], doc["category"])
    )

db.commit()
db.close()
print("Knowledge base seeded with", len(documents), "documents.")

What are these documents about?

You are seeding four documents covering context engineering, harness engineering, the difference between context and prompt engineering, and database schema design. These give your AI a knowledge base it can search when answering domain-specific questions.

  • Save seed_knowledge.py.
  • Before you run this, what do you expect the output to be?
  • Run the seed script by executing this in your terminal:
python seed_knowledge.py

You should see:

Knowledge base seeded with 4 documents.

Seeing an error?

If you get ModuleNotFoundError: No module named 'database', make sure you are running the command from your project folder (the same directory that contains database.py).

If you see a SQL error about a duplicate entry, delete assistant.db and run the script again. help me fix this seed script error

Build the context engine module

The context engine is a separate module whose job is to assemble everything the AI should see before generating a response. It combines system instructions, retrieved knowledge documents, and conversation history into a single ordered list of messages.

  • Create a new file called context_engine.py in your project folder.
  • Paste the following code:
from database import get_conversation_messages, get_relevant_knowledge


SYSTEM_PROMPT = """You are a knowledgeable AI assistant specializing in AI engineering, context engineering, and software development. Follow these rules:

1. Answer questions accurately using the knowledge provided in context.
2. Reference specific concepts from the provided documents when relevant.
3. If you do not know something, say so clearly rather than guessing.
4. Keep responses concise but informative.
5. When discussing technical topics, provide practical examples."""


def assemble_context(db, conversation_id, current_query):
    # Start with an empty message list
    messages = []
    # Retrieve relevant knowledge documents based on the user's query
    knowledge_docs = get_relevant_knowledge(db, current_query)
    knowledge_section = ""
    if knowledge_docs:
        knowledge_section = "\n\nRelevant knowledge documents:\n"
        for doc in knowledge_docs:
            knowledge_section += f"\n--- {doc['title']} ---\n{doc['content']}\n"
    # Combine the system prompt with any retrieved knowledge
    system_content = SYSTEM_PROMPT + knowledge_section
    messages.append({"role": "system", "content": system_content})
    # Load conversation history so the AI remembers previous turns
    history = get_conversation_messages(db, conversation_id, limit=10)
    for msg in history:
        messages.append({"role": msg["role"], "content": msg["content"]})
    return messages

What does assemble_context do?

This function builds the complete context window in three layers:

  • System prompt: behavioral instructions telling the AI how to respond.
  • Retrieved knowledge: documents from the knowledge base that match the user's current query, appended to the system prompt.
  • Conversation history: the last 10 messages so the AI maintains memory.

The order matters. System instructions come first, then knowledge, then history. This is context engineering in action.

Why a separate module?

Keeping context assembly in its own file follows the separation of concerns principle. Your Flask routes handle HTTP. The context engine handles what the AI sees. This makes each piece easier to test and modify independently.

  • Save context_engine.py.
  • Verify the module imports correctly by running:
python -c "from context_engine import assemble_context; print('Context engine ready!')"

You should see Context engine ready! printed.

Getting an ImportError?

Make sure context_engine.py is saved in the same folder as database.py. Both files must be in the same directory for the import to work. help me fix this import error

Wire up the context engine and add comparison

Now you will connect the context engine to your chat route. You will also add a comparison endpoint that shows the same question answered with and without context engineering, side by side.

  • Open app.py in VS Code.
  • Find the import lines at the top of the file. Replace the first three lines with these updated imports:
import sqlite3
from flask import Flask, render_template, request, jsonify
from database import init_db, get_db, store_message, get_conversation_messages, create_conversation, store_context_log
from context_engine import assemble_context
from ollama import chat
  • Now find the existing chat_route function. Replace the entire function (from @app.route("/chat") through its return statement) with this updated version:
@app.route("/chat", methods=["POST"])
def chat_route():
    data = request.get_json()
    user_message = data["message"]
    conversation_id = data["conversation_id"]
    db = get_db()
    store_message(db, conversation_id, "user", user_message)
    # Use the context engine instead of raw history
    context_messages = assemble_context(db, conversation_id, user_message)
    response = chat(model="deepseek-r1:8b", messages=context_messages)
    assistant_content = response.message.content
    message_id = store_message(db, conversation_id, "assistant", assistant_content)
    # Log the assembled context for observability
    context_text = "\n\n".join([f"[{m['role'].upper()}]\n{m['content']}" for m in context_messages])
    token_estimate = len(context_text) // 4
    store_context_log(db, message_id, context_text, token_estimate)
    return jsonify({"response": assistant_content, "message_id": message_id})

What changed in the chat route?

  • Instead of manually building a messages list from history, the route calls assemble_context which adds system instructions, relevant knowledge, and history.
  • After the AI responds, the route logs the full context (serialized as text) and an estimated token count to context_logs.
  • The response now includes message_id so the frontend can fetch the context log later.
  • Save app.py.
  • Stop your Flask server if it is still running (press Ctrl+C in the terminal).
  • Delete the old database so the new tables are created fresh alongside your seed data by running:
rm assistant.db
python seed_knowledge.py
  • Restart the Flask server by running:
python app.py
  • Open http://127.0.0.1:5000 in your browser.
  • Before you type a question, predict: will the AI be able to explain context engineering now, or will it still give a generic answer?
  • Type what is context engineering? and press Enter.

The AI now gives a detailed, accurate answer referencing the four pillars (Instructions, Retrieval, Memory, Available Tools) from your knowledge base. This is the context engine at work.

Still getting generic answers?

Make sure you ran python seed_knowledge.py after deleting the old database. Without seed data, the knowledge table is empty and there is nothing to retrieve.

Check that your context_engine.py imports from database match the function names exactly: get_conversation_messages and get_relevant_knowledge. help me debug why my context engine isn't retrieving knowledge

Now add the comparison endpoint so you can see both responses side by side.

  • In app.py, add this new route directly below the chat_route function (before the if __name__ block):
@app.route("/compare", methods=["POST"])
def compare_route():
    data = request.get_json()
    user_message = data["message"]
    conversation_id = data["conversation_id"]
    db = get_db()
    # Get a naive response with no context engineering
    naive_messages = [{"role": "user", "content": user_message}]
    naive_response = chat(model="deepseek-r1:8b", messages=naive_messages)
    naive_content = naive_response.message.content
    # Get a context-engineered response
    context_messages = assemble_context(db, conversation_id, user_message)
    context_response = chat(model="deepseek-r1:8b", messages=context_messages)
    context_content = context_response.message.content
    return jsonify({"naive": naive_content, "context_engineered": context_content})

What does the compare route do?

It asks the same model the same question twice. Once with just the raw user message (no system prompt, no knowledge, no history). Once with the full context-engineered pipeline. The response contains both answers so you can see the difference side by side.

  • Add one more route directly below compare_route for fetching context logs:
@app.route("/context/<int:message_id>")
def get_context(message_id):
    db = get_db()
    cursor = db.execute(
        "SELECT context_assembled, token_estimate FROM context_logs WHERE message_id = ?",
        (message_id,)
    )
    row = cursor.fetchone()
    if row is None:
        return jsonify({"error": "No context log found"}), 404
    return jsonify({"context": row["context_assembled"], "token_estimate": row["token_estimate"]})
  • Save app.py.

Now upgrade the frontend to support comparison and context inspection.

  • Open templates/index.html and delete all its contents.
  • Replace it with the following styled interface. Start with the head section and CSS:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Context-Engineered AI Assistant</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
        header { background: #16213e; padding: 16px 24px; border-bottom: 1px solid #0f3460; }
        header h1 { font-size: 1.2rem; color: #e94560; }
        header p { font-size: 0.8rem; color: #888; margin-top: 4px; }
        #chat-container { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
        .message { max-width: 70%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; }
        .message.user { align-self: flex-end; background: #0f3460; }
        .message.assistant { align-self: flex-start; background: #16213e; border: 1px solid #0f3460; }
        .message .inspect-btn { display: block; margin-top: 8px; font-size: 0.75rem; color: #e94560; cursor: pointer; background: none; border: none; text-decoration: underline; }
        .context-panel { margin-top: 8px; background: #0d1b2a; border: 1px solid #e94560; border-radius: 8px; padding: 12px; font-size: 0.8rem; color: #aaa; white-space: pre-wrap; display: none; }
        .context-panel.visible { display: block; }
        .context-panel .token-count { color: #e94560; font-weight: bold; }
        #input-area { padding: 16px 24px; background: #16213e; border-top: 1px solid #0f3460; display: flex; gap: 12px; }
        #input-area input { flex: 1; padding: 12px 16px; border-radius: 8px; border: 1px solid #0f3460; background: #1a1a2e; color: #eee; font-size: 1rem; }
        #input-area input:focus { outline: none; border-color: #e94560; }
        #input-area button { padding: 12px 24px; border-radius: 8px; border: none; background: #e94560; color: white; font-size: 1rem; cursor: pointer; }
        #input-area button:hover { background: #c23152; }
        #compare-btn { padding: 12px 16px; border-radius: 8px; border: 1px solid #e94560; background: transparent; color: #e94560; font-size: 0.9rem; cursor: pointer; }
        .compare-result { align-self: stretch; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
        .compare-result div { padding: 12px; border-radius: 8px; font-size: 0.9rem; line-height: 1.5; white-space: pre-wrap; }
  • Continue directly below with the remaining styles and body HTML:
        .compare-result .naive { background: #2d1b1b; border: 1px solid #6b3030; }
        .compare-result .engineered { background: #1b2d1b; border: 1px solid #306b30; }
        .compare-result h4 { font-size: 0.75rem; text-transform: uppercase; margin-bottom: 8px; color: #888; }
    </style>
</head>
<body>
    <header>
        <h1>Context-Engineered AI Assistant</h1>
        <p>Powered by deepseek-r1:8b via Ollama | Context Engineering in action</p>
    </header>
    <div id="chat-container"></div>
    <div id="input-area">
        <input type="text" id="user-input" placeholder="Type a message..." autocomplete="off">
        <button id="send-btn" onclick="sendMessage()">Send</button>
        <button id="compare-btn" onclick="compareMessage()">Compare</button>
    </div>
  • Now add the JavaScript. Paste this directly below the HTML you just added:
    <script>
        const conversationId = {{ conversation_id }};
        const chatContainer = document.getElementById("chat-container");
        const userInput = document.getElementById("user-input");
        userInput.addEventListener("keypress", function(e) {
            if (e.key === "Enter") sendMessage();
        });
        function addMessage(role, content, messageId) {
            const div = document.createElement("div");
            div.className = "message " + role;
            div.textContent = content;
            if (role === "assistant" && messageId) {
                const btn = document.createElement("button");
                btn.className = "inspect-btn";
                btn.textContent = "Show Context";
                btn.onclick = function() { toggleContext(messageId, div); };
                div.appendChild(btn);
                const panel = document.createElement("div");
                panel.className = "context-panel";
                panel.id = "context-" + messageId;
                div.appendChild(panel);
            }
            chatContainer.appendChild(div);
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }
  • Continue the script with the message-sending and comparison functions:
        async function sendMessage() {
            const message = userInput.value.trim();
            if (!message) return;
            addMessage("user", message);
            userInput.value = "";
            const res = await fetch("/chat", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({message: message, conversation_id: conversationId})
            });
            const data = await res.json();
            addMessage("assistant", data.response, data.message_id);
        }
        async function compareMessage() {
            const message = userInput.value.trim();
            if (!message) return;
            addMessage("user", "[COMPARE] " + message);
            userInput.value = "";
            const res = await fetch("/compare", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({message: message, conversation_id: conversationId})
            });
            const data = await res.json();
            const div = document.createElement("div");
            div.className = "compare-result";
            div.innerHTML = "<div class='naive'><h4>Without Context Engineering</h4>" + data.naive + "</div><div class='engineered'><h4>With Context Engineering</h4>" + data.context_engineered + "</div>";
            chatContainer.appendChild(div);
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }
  • Finish the file with the context inspection function and closing tags:
        async function toggleContext(messageId, parentDiv) {
            const panel = document.getElementById("context-" + messageId);
            if (panel.classList.contains("visible")) {
                panel.classList.remove("visible");
                return;
            }
            const res = await fetch("/context/" + messageId);
            const data = await res.json();
            panel.innerHTML = "<span class='token-count'>Estimated tokens: " + data.token_estimate + "</span>\n\n" + data.context;
            panel.classList.add("visible");
        }
    </script>
</body>
</html>

What do the JavaScript functions do?

  • addMessage: displays a chat bubble. For assistant messages, it also creates a "Show Context" button and a hidden panel.
  • sendMessage: posts the user's input to your /chat route and displays the response.
  • compareMessage: posts to the /compare route and displays both naive and context-engineered answers in a side-by-side grid.
  • toggleContext: fetches the stored context log for a message and reveals it in a collapsible panel below the response.
  • Save templates/index.html.
  • Stop the Flask server with Ctrl+C and restart it:
python app.py
  • Open http://127.0.0.1:5000 in your browser. You should see a dark-themed chat interface with a Send button and a Compare button.
  • Type what is context engineering? in the input field.
  • Instead of clicking Send, click the Compare button.
  • Predict: will the two panels look the same or different?

You will see two side-by-side panels. The left panel ("Without Context Engineering") gives a vague, generic answer. The right panel ("With Context Engineering") gives a detailed answer referencing the four pillars and specific concepts from your knowledge base.

That difference is the entire value of context engineering, made visible in one comparison.

Compare button not showing results?

The compare endpoint calls the AI model twice, so it takes longer than a normal message. Wait 15-30 seconds for both responses to generate.

If you see a JavaScript error in the browser console, check that your app.py has both the compare_route and get_context functions defined before the if __name__ == "__main__" block. help me fix my compare endpoint

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files: database.py, seed_knowledge.py, context_engine.py, and app.py.

ⓧ I'd like to double check the full code

Here is the full context_engine.py file for reference:

from database import get_conversation_messages, get_relevant_knowledge


SYSTEM_PROMPT = """You are a knowledgeable AI assistant specializing in AI engineering, context engineering, and software development. Follow these rules:

1. Answer questions accurately using the knowledge provided in context.
2. Reference specific concepts from the provided documents when relevant.
3. If you do not know something, say so clearly rather than guessing.
4. Keep responses concise but informative.
5. When discussing technical topics, provide practical examples."""


def assemble_context(db, conversation_id, current_query):
    messages = []
    knowledge_docs = get_relevant_knowledge(db, current_query)
    knowledge_section = ""
    if knowledge_docs:
        knowledge_section = "\n\nRelevant knowledge documents:\n"
        for doc in knowledge_docs:
            knowledge_section += f"\n--- {doc['title']} ---\n{doc['content']}\n"
    system_content = SYSTEM_PROMPT + knowledge_section
    messages.append({"role": "system", "content": system_content})
    history = get_conversation_messages(db, conversation_id, limit=10)
    for msg in history:
        messages.append({"role": msg["role"], "content": msg["content"]})
    return messages

Here is the full app.py. Scroll through and confirm your file matches:

import sqlite3
from flask import Flask, render_template, request, jsonify
from database import init_db, get_db, store_message, get_conversation_messages, create_conversation, store_context_log
from context_engine import assemble_context
from ollama import chat

app = Flask(__name__)

init_db()

@app.route("/")
def index():
    db = get_db()
    conversation = create_conversation(db)
    return render_template("index.html", conversation_id=conversation["id"])

@app.route("/chat", methods=["POST"])
def chat_route():
    data = request.get_json()
    user_message = data["message"]
    conversation_id = data["conversation_id"]
    db = get_db()
    store_message(db, conversation_id, "user", user_message)
    context_messages = assemble_context(db, conversation_id, user_message)
    response = chat(model="deepseek-r1:8b", messages=context_messages)
    assistant_content = response.message.content
    message_id = store_message(db, conversation_id, "assistant", assistant_content)
    context_text = "\n\n".join([f"[{m['role'].upper()}]\n{m['content']}" for m in context_messages])
    token_estimate = len(context_text) // 4
    store_context_log(db, message_id, context_text, token_estimate)
    return jsonify({"response": assistant_content, "message_id": message_id})

@app.route("/compare", methods=["POST"])
def compare_route():
    data = request.get_json()
    user_message = data["message"]
    conversation_id = data["conversation_id"]
    db = get_db()
    naive_messages = [{"role": "user", "content": user_message}]
    naive_response = chat(model="deepseek-r1:8b", messages=naive_messages)
    naive_content = naive_response.message.content
    context_messages = assemble_context(db, conversation_id, user_message)
    context_response = chat(model="deepseek-r1:8b", messages=context_messages)
    context_content = context_response.message.content
    return jsonify({"naive": naive_content, "context_engineered": context_content})

@app.route("/context/<int:message_id>")
def get_context(message_id):
    db = get_db()
    cursor = db.execute(
        "SELECT context_assembled, token_estimate FROM context_logs WHERE message_id = ?",
        (message_id,)
    )
    row = cursor.fetchone()
    if row is None:
        return jsonify({"error": "No context log found"}), 404
    return jsonify({"context": row["context_assembled"], "token_estimate": row["token_estimate"]})

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

Your context engine is live and proving its value. Next up, you will make the invisible visible by building a Context Inspector that shows exactly what tokens the model receives on each turn.

Secret mission

Add a Context Inspector

Production AI systems need observability. You cannot improve what you cannot see. Can you verify that the Context Inspector you built actually reveals the full context window the model receives on each turn?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Your SQLite database is a local file, Ollama runs on your machine for free, and Flask only uses resources while the server is active.

Resources you used:

  • Project folder on your Desktop (contains app.py, database.py, context_engine.py, seed_knowledge.py, and templates/index.html).
  • SQLite database file (assistant.db) inside your project folder.
  • Python virtual environment inside your project folder.
  • Flask dev server (running process).

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your context-engineered assistant.

  • Your chatbot works whenever Ollama is running on your machine.
  • Start a new chat session anytime by running python app.py from your project folder.
  • Your conversation history and knowledge base persist in assistant.db.

Pause - I'll come back to this later

Shut down the running Flask server to free up your terminal. Your files and database stay on disk, ready to restart.

  • In the terminal where Flask is running, press Ctrl+C to stop the server.
  • When you want to resume, open your terminal, navigate to your project folder, activate the virtual environment, and run:
python app.py

Your conversations, knowledge base, and context logs will still be there.

Delete - I don't want to use this again

Remove all project files, the database, and the virtual environment completely.

  • In the terminal where Flask is running, press Ctrl+C to stop the server.
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

On Windows, use this command instead:

cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_FOLDER="your project folder name"]]

This removes all Python files, the virtual environment, the assistant.db database, and the HTML template. Nothing else on your system is affected.

That's a Wrap!

That's a Wrap!

Nice work. You just built a context-engineered AI assistant from scratch using Python, Flask, SQLite, and Ollama. You proved that the same model gives dramatically better answers when you engineer what it sees.

You've learned how to:

  • Build a full-stack Python web application with Flask routing, HTML templates, and a SQLite database storing conversations and messages with foreign key relationships.
  • Implement context engineering by assembling system instructions, retrieved knowledge documents, and conversation history into a structured context window before each AI call.
  • Demonstrate measurably better AI responses through a side-by-side comparison showing the same question answered with and without context engineering.
  • Built observability into an AI system with the Context Inspector, revealing exactly what tokens the model receives on each turn (Secret Mission).

Ready to quiz yourself?