<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Oswin Heman-Ackah</title>
    <description>The latest articles on Forem by Oswin Heman-Ackah (@ackahman).</description>
    <link>https://forem.com/ackahman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3505158%2F3982e211-8e28-4def-bc7f-fddc7c6881a1.jpg</url>
      <title>Forem: Oswin Heman-Ackah</title>
      <link>https://forem.com/ackahman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ackahman"/>
    <language>en</language>
    <item>
      <title>Building a chatbot with Python (Backend)</title>
      <dc:creator>Oswin Heman-Ackah</dc:creator>
      <pubDate>Tue, 16 Sep 2025 08:37:38 +0000</pubDate>
      <link>https://forem.com/ackahman/building-a-chatbot-with-python-backend-119b</link>
      <guid>https://forem.com/ackahman/building-a-chatbot-with-python-backend-119b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Documentation: Backend (backend.py)&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;This script processes PDF documents into vector embeddings and builds a FAISS index for semantic search.&lt;br&gt;
It is the offline preprocessing pipeline for the Indaba RAG chatbot.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Responsibilities&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load PDFs from a folder.&lt;/li&gt;
&lt;li&gt;Extract raw text using PyPDF2.&lt;/li&gt;
&lt;li&gt;Chunk large documents into smaller overlapping text segments.&lt;/li&gt;
&lt;li&gt;Convert chunks into embeddings using SentenceTransformers.&lt;/li&gt;
&lt;li&gt;Build and persist a &lt;em&gt;FAISS index&lt;/em&gt; for similarity search.&lt;/li&gt;
&lt;li&gt;Save the raw chunks for later retrieval.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Step-by-Step Breakdown&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imports and Setup&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import pickle
import numpy as np
from PyPDF2 import PdfReader
from sentence_transformers import SentenceTransformer
import faiss

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;os&lt;/strong&gt; → file system operations.&lt;br&gt;
&lt;strong&gt;pickle&lt;/strong&gt;→ save preprocessed chunks.&lt;br&gt;
&lt;strong&gt;numpy&lt;/strong&gt; → numerical array handling.&lt;br&gt;
&lt;strong&gt;PyPDF2&lt;/strong&gt;→ extract text from PDF files.&lt;br&gt;
&lt;strong&gt;SentenceTransformer&lt;/strong&gt; → embedding model (all-MiniLM-L6-v2).&lt;br&gt;
&lt;strong&gt;faiss&lt;/strong&gt; → efficient similarity search.&lt;/p&gt;

&lt;p&gt;Constants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;embedder = SentenceTransformer("all-MiniLM-L6-v2")
INDEX_FILE = "faiss_index.bin"
CHUNKS_FILE = "chunks.pkl"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;embedder is the model instance; loading this, downloads model weights (first run may take time).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;INDEX_FILE&lt;/em&gt; and &lt;em&gt;CHUNKS_FILE&lt;/em&gt; defines where to save FAISS index and chunks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Function to Load PDF&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def load_pdf(file_path):
    pdf = PdfReader(file_path)
    text = ""
    for page in pdf.pages:
        text += page.extract_text() + "\n"
    return text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Reads a PDF file with PyPDF2.&lt;/li&gt;
&lt;li&gt;Extracts text page by page.&lt;/li&gt;
&lt;li&gt;Returns the full document text as a string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Function for Text Chunking&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def chunk_text(text, chunk_size=500, overlap=100):
    chunks = []
    start = 0
    while start &amp;lt; len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Splits the text into chunks of chunk_size characters, shifting by chunk_size - overlap each time (so consecutive chunks overlap by overlap characters).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Representation:&lt;br&gt;
Chunk 1 = 0–500&lt;br&gt;
Chunk 2 = 400–900 (100 overlap)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Full Pipeline Info&lt;/strong&gt;&lt;br&gt;
Walkthrough of the function:&lt;/p&gt;

&lt;p&gt;1.Collect chunks for all PDFs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pdf_folder = "vault"   
 #This is the folder/path pdfs are stored in.

all_chunks = []
for filename in os.listdir(pdf_folder):
    if filename.endswith(".pdf"):
        text = load_pdf(os.path.join(pdf_folder, filename))
        chunks = chunk_text(text)
        all_chunks.extend(chunks)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Extracts text and chunks for each PDF and keeps all chunks in &lt;strong&gt;all_chunks list&lt;/strong&gt; as strings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Order matters (index ids align with order).&lt;/p&gt;

&lt;p&gt;2.Embed chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vectors = embedder.encode(all_chunks)
   vectors = np.array(vectors)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;embedder.encode(list_of_texts) returns a list/array of vectors. &lt;em&gt;By default, it returns float32 or float64 depending on version&lt;/em&gt; — FAISS expects float32. In practice it's safer to force dtype float32.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Important: embedding all chunks at once can OOM if you have many chunks. Use batching:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; vectors = embedder.encode(all_chunks, batch_size=32, show_progress_bar=True)
 vectors = np.array(vectors).astype('float32')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3.Create FAISS index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dim = vectors.shape[1]
   index = faiss.IndexFlatL2(dim)
   index.add(vectors)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Basic)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a FAISS index and adds all chunk vectors into the index. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Technical)&lt;br&gt;
&lt;strong&gt;IndexFlatL2&lt;/strong&gt; = exact (brute-force) nearest neighbor search using L2 distance. Works for small-to-medium collections. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pros: simple and exact. &lt;/li&gt;
&lt;li&gt;Cons: slow on large collections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;index.add(vectors)&lt;/strong&gt; adds vectors in the same order as all_chunks. FAISS internal ids = 0..N-1 in that order — that’s how you map back to chunks.&lt;/p&gt;

&lt;p&gt;4.Save index and chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  faiss.write_index(index, INDEX_FILE)
   with open(CHUNKS_FILE, "wb") as f:
       pickle.dump(all_chunks, f)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saves FAISS index to faiss_index.bin.&lt;br&gt;
Saves chunks (raw text) to chunks.pkl.&lt;/p&gt;

&lt;p&gt;These files are later loaded by the Streamlit frontend on runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to run this script&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  make sure your PDFs are in docs/
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;python -m backend&lt;/code&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>python</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Building a Chatbot with Python (Frontend)</title>
      <dc:creator>Oswin Heman-Ackah</dc:creator>
      <pubDate>Tue, 16 Sep 2025 03:34:38 +0000</pubDate>
      <link>https://forem.com/ackahman/building-a-chatbot-with-python-frontend-2f2c</link>
      <guid>https://forem.com/ackahman/building-a-chatbot-with-python-frontend-2f2c</guid>
      <description>&lt;p&gt;&lt;strong&gt;Documentation: Frontend (frontend.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This file defines the Streamlit-based frontend for the Indaba Retrieval-Augmented Generation (RAG) chatbot.&lt;br&gt;
It provides the user interface, manages queries, retrieves relevant chunks from the FAISS index, and generates answers using the Groq LLM API.&lt;br&gt;
It also applies a custom CSS theme for a modern UI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Responsibilities&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the FAISS index and pre-processed text chunks.&lt;/li&gt;
&lt;li&gt;Take user input (questions) through a chat-like form.&lt;/li&gt;
&lt;li&gt;Retrieve the most relevant chunks using semantic search.&lt;/li&gt;
&lt;li&gt;Pass retrieved chunks into the Groq-powered LLM to generate answers.&lt;/li&gt;
&lt;li&gt;Display responses in a styled Streamlit interface.&lt;/li&gt;
&lt;li&gt;Provide a button to clear chat history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Step-by-Step Breakdown&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;1. Imports and Setup&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import streamlit as st
import pickle
import faiss
from sentence_transformers import SentenceTransformer
from groq import Groq
import os
# from dotenv import load_dotenv

# load_dotenv()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;What is happening:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;streamlit&lt;/strong&gt;→ UI framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pickle&lt;/strong&gt;→ loads pre-saved chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;faiss&lt;/strong&gt; → similarity search engine for embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SentenceTransformer&lt;/strong&gt; → embedding model (all-MiniLM-L6-v2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq&lt;/strong&gt; → client to access LLM API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;os&lt;/strong&gt; → (optional) environment variable access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Unlike local.env use, API keys are pulled from st.secrets for deployment safety. And that is why the &lt;strong&gt;dotenv&lt;/strong&gt; variable is commented out.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Load FAISS Index and Chunks&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
INDEX_FILE = "faiss_index.bin"
CHUNKS_FILE = "chunks.pkl"
embedder = SentenceTransformer("all-MiniLM-L6-v2", device="cpu")

index = faiss.read_index(INDEX_FILE)
with open(CHUNKS_FILE, "rb") as f:
    chunks = pickle.load(f)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Loads FAISS index (vector database).&lt;/li&gt;
&lt;li&gt;Loads preprocessed document chunks (chunks.pkl).&lt;/li&gt;
&lt;li&gt;Ensures embeddings match the index by using the same model.&lt;/li&gt;
&lt;li&gt;Forces CPU for compatibility on Streamlit Cloud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Initialize Groq Client&lt;/strong&gt;&lt;br&gt;
client = Groq(api_key=st.secrets["grok"]["api_key"])&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieves API key from Streamlit secrets.&lt;/li&gt;
&lt;li&gt;Sets up Groq client for LLM queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Semantic Search Function&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def search_index(query, k=10):
    q_vec = embedder.encode([query])
    D, I = index.search(q_vec, k)
    return [chunks[i] for i in I[0]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Encodes the query into a vector.&lt;/li&gt;
&lt;li&gt;Searches FAISS for the top k most relevant chunks.&lt;/li&gt;
&lt;li&gt;Returns those chunks for answer generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. LLM Answer Generation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def generate_answer(question, context_chunks):
    context = "\n\n".join(context_chunks)
    prompt = (
        f"Answer the question based on the context provided. "
        "If the question is not related to the context in any way, do NOT attempt to answer. "
        "Instead, strictly reply: 'My knowledge base does not have information about this. Please contact the technical team.'\n\n"
        f"Context: {context}\n\nQuestion: {question}\nAnswer:"
    )
    response = client.chat.completions.create(
        messages=[{"role": "user", "content": prompt}],
        model="llama-3.3-70b-versatile",
    )
    return response.choices[0].message.content.strip()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Builds a RAG prompt with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieved context.&lt;/li&gt;
&lt;li&gt;Question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sends to &lt;strong&gt;Groq’s LLaMA-3.3-70B model&lt;/strong&gt;.&lt;br&gt;
Returns a &lt;strong&gt;clean answer.&lt;/strong&gt;&lt;br&gt;
Enforces that if &lt;strong&gt;no context exists&lt;/strong&gt; → &lt;strong&gt;chatbot says&lt;/strong&gt;:&lt;br&gt;
&lt;em&gt;“My knowledge base does not have information about this. Please contact the technical team.”&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Custom Chat UI (CSS)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;st.markdown(
    """
&amp;lt;style&amp;gt;
...
&amp;lt;/style&amp;gt;
""",
    unsafe_allow_html=True,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Dark theme with neon-blue highlights.&lt;/li&gt;
&lt;li&gt;Styles input box, buttons, and retrieved chunks.&lt;/li&gt;
&lt;li&gt;Enhances readability and adds a polished UI feel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;7. Streamlit UI: Title &amp;amp; Instructions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;st.title("🤖 Indaba")
st.write("Ask questions based on Discrete Mathematics.")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Displays app title.&lt;/li&gt;
&lt;li&gt;Provides short instructions to user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;8. Question Input Form&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;with st.form(key="chat_form", clear_on_submit=True):
    question = st.text_input("Your question:", key="question_input")
    submit_button = st.form_submit_button("Send")

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Input form for user questions.&lt;/li&gt;
&lt;li&gt;Clears text box on submit.&lt;/li&gt;
&lt;li&gt;Controlled by a Send button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;9. Main Chat Logic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if submit_button and question:
    retrieved = search_index(question)
    answer = generate_answer(question, retrieved)

    st.markdown("### 🤖 Answer")
    st.write(answer)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;On submit:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieves relevant chunks.&lt;/li&gt;
&lt;li&gt;Generates answer from Groq LLM.&lt;/li&gt;
&lt;li&gt;Displays result under “🤖 Answer”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;# Clear Chat Button&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if st.button("Clear Chat"):
    st.session_state.messages = []
    st.rerun()

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Resets chat state.&lt;/li&gt;
&lt;li&gt;Reruns app for a fresh session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Workflow (Frontend)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User submits a question.&lt;/li&gt;
&lt;li&gt;Query is embedded and searched in FAISS.&lt;/li&gt;
&lt;li&gt;Top chunks are retrieved.&lt;/li&gt;
&lt;li&gt;Chunks + question are passed into LLM.&lt;/li&gt;
&lt;li&gt;LLM generates an answer based only on retrieved knowledge.&lt;/li&gt;
&lt;li&gt;Answer is displayed in a styled interface.&lt;/li&gt;
&lt;li&gt;User can clear chat anytime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Other Notes&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Frontend relies on the backend (index_docs.py) having run beforehand. It doesn’t rebuild the index. &lt;/li&gt;
&lt;li&gt;Chat memory is session-based only (clears on refresh).&lt;/li&gt;
&lt;li&gt;Environment variable GROQ_API_KEY must be set in when running locally in the:&lt;/li&gt;
&lt;li&gt;Local .env file.
Otherwise, it is set in streamlit secrets as:&lt;/li&gt;
&lt;li&gt;Streamlit Secrets ["grok"]["api_key"] during deployment.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>documentation</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
