This notebook creates a "working prototype" of an AI-powered chatbot, the "AI Mentor," for the City of Brampton's HR Proof of Concept.
The goal is to build a RAG (Retrieval-Augmented Generation) model that is:
This notebook will:
Before running this notebook, you must complete these five manual setup steps.
Repo: https://github.com/burnsgregm/BramptonHR
This first deployment might fail (because app.py doesn't exist yet), but that's fine. It has now "linked" the repo. Every time this notebook pushes an update to GitHub, Streamlit Cloud will automatically re-deploy the live app.
# --- 1.3 Install Libraries (final, clean Colab version) ---
print("Uninstalling old/conflicting packages...")
!pip uninstall -y langchain langchain-core langchain-community langchain-text-splitters langchain-classic langchain-google-genai
print("\nUpgrading pip tooling...")
!pip install -q -U pip setuptools wheel jedi
print("\nInstalling pinned, compatible versions...")
!pip install -q -U \
"langchain==0.3.27" \
"langchain-core==0.3.79" \
"langchain-community==0.3.27" \
"langchain-text-splitters==0.3.11" \
"langchain-google-genai==2.1.5" \
"faiss-cpu" \
"pypdf" \
"beautifulsoup4" \
"streamlit==1.*"
print("\nInstall complete. Restart the runtime, then run Section 1.4.")
# --- 1.4 Import Libraries ---
os.environ["USER_AGENT"] = "burnsgregm-bramptonhr/0.1"
# Core Python & File Management
import os
import textwrap
from getpass import getpass
# Colab-Specific
from google.colab import userdata
# Data Ingestion & Processing
import pypdf
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Vector Store
from langchain_community.vectorstores import FAISS
from langchain_google_genai import GoogleGenerativeAIEmbeddings
# LLM / Chat / RAG Components (new style)
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains.retrieval import create_retrieval_chain
print("All libraries imported successfully and ready to roll!")
# --- 1.5 Global Configurations ---
# --- 1. GitHub Configuration ---
GITHUB_USERNAME = "burnsgregm"
GITHUB_REPO_NAME = "BramptonHR"
GITHUB_TOKEN_NAME = "GITHUB_TOKEN" # Colab Secret name
GITHUB_EMAIL = "burnsgregm@gmail.com"
GIT_COMMIT_MESSAGE = "Build: Initial PoC build from Colab factory"
# --- 2. Google AI Configuration ---
GEMINI_API_KEY_NAME = "GEMINI_API_KEY" # Colab Secret name
GEMINI_EMBEDDING_MODEL = "models/text-embedding-004"
GEMINI_LLM_MODEL = "models/gemini-2.5-pro"
# ---
# --- 3. Project File & Path Configuration ---
REPO_PATH = f"/content/{GITHUB_REPO_NAME}"
DOCS_DIR = "docs"
DOCS_PATH_IN_REPO = f"{REPO_PATH}/{DOCS_DIR}"
VECTORSTORE_PATH = "/content/vectorstore"
DOC_FILENAMES = [
"Anti-Racism and Inclusion Policy ANI-140.pdf",
"Employee Code of Conduct Handbook.pdf",
"Employee Code of Conduct HRM-100.pdf",
"Recruiting and Retaining Top Talent HRM-160.pdf",
"Respectful Workplace Policy HRM-150.pdf",
"Salary Administration Policy HRM-210.pdf"
]
GBA_PLUS_URLS = [
"https://www.brampton.ca/EN/City-Hall/Equity-Office/Pages/Training-and-Education-Progress-Updates.aspx",
"https://www.canada.ca/en/women-gender-equality/gender-based-analysis-plus/take-course.html"
]
# --- 4. Streamlit App Configuration ---
APP_TITLE = "Brampton L&D Co-Pilot (PoC)"
# --- 5. RAG Prompt Configuration (Refactored for modern API) ---
RAG_PROMPT_TEMPLATE = """
SYSTEM INSTRUCTION:
You are the "AI Mentor," a helpful and safe AI assistant for City of Brampton employees.
Your job is to answer questions *only* based on the official, approved policy documents provided as context.
DO NOT, under any circumstances, use any outside knowledge or make up information.
Your answer MUST follow these rules:
1. **Source Restriction:** Base your answer *only* on the text provided in the "<context>" section.
2. **No Outside Knowledge:** If the answer is not in the context, you MUST state: "I'm sorry, I do not have access to that specific information in the approved policy documents. Please contact your HR Advisor."
3. **Proactive "Golden Thread":** If (and *only* if) the user's question is about "hiring," "recruiting," or "job posting," you MUST first answer their question, and *then* you MUST add the following proactive nudge:
"Additionally, please be aware that Policy HRM-160 requires all hiring committee members to complete the 'Mandatory Recruitment and Diversity Learning Series.' You can find this module on the L&D portal."
4. **Tone:** Be professional, helpful, and concise.
<context>
{context}
</context>
Question: {input}
HELPFUL ANSWER (Based *only* on the context):
"""
print("Global configurations set.")
This section contains the core logic for the project. It will configure the environment, load all source documents from your GitHub repo, and build the RAG (Retrieval-Augmented Generation) pipeline.
# --- 2.1 Configure API & Git Environment ---
print("Accessing Colab secrets...")
# Get secrets from Colab
try:
GOOGLE_API_KEY = userdata.get(GEMINI_API_KEY_NAME)
GITHUB_TOKEN = userdata.get(GITHUB_TOKEN_NAME)
except Exception as e:
print(f"ERROR: Could not find secrets. Did you set {GEMINI_API_KEY_NAME} and {GITHUB_TOKEN_NAME} in Colab?")
raise e
# Set environment variable for Google AI
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
print("Cloning GitHub repository...")
# Clone the GitHub repo to the Colab filesystem
!rm -rf {REPO_PATH} # Remove repo if it already exists for a clean clone
# --- THIS LINE IS THE FIX ---
GIT_CLONE_URL = f"https://{GITHUB_TOKEN}@github.com/{GITHUB_USERNAME}/{GITHUB_REPO_NAME}.git"
# --- (The previous version was missing '://' after 'https') ---
!git clone {GIT_CLONE_URL} {REPO_PATH}
print("Configuring Git...")
# Configure Git for committing
!git config --global user.email "{GITHUB_EMAIL}"
!git config --global user.name "{GITHUB_USERNAME}"
print("Section 2.1 complete.")
# --- 2.2 Ingest & Vectorize Documents ---
print("Loading documents from GitHub repo...")
# 1. Create file paths for PDFs
pdf_paths = [os.path.join(DOCS_PATH_IN_REPO, f) for f in DOC_FILENAMES]
# 2. Load PDFs
pdf_docs = []
for path in pdf_paths:
if os.path.exists(path):
loader = PyPDFLoader(path)
pdf_docs.extend(loader.load())
else:
print(f"Warning: Document not found at {path}")
print(f"Loaded {len(pdf_docs)} pages from {len(pdf_paths)} PDF documents.")
# 3. Load Web Pages
web_loaders = [WebBaseLoader(url) for url in GBA_PLUS_URLS]
web_docs = []
for loader in web_loaders:
try:
web_docs.extend(loader.load())
except Exception as e:
print(f"Warning: Could not load URL {loader.web_path}: {e}")
print(f"Loaded {len(web_docs)} web documents.")
all_docs = pdf_docs + web_docs
print(f"Loaded {len(all_docs)} total document chunks.")
# 4. Split Documents
print("Splitting all documents...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1500,
chunk_overlap=200
)
splits = text_splitter.split_documents(all_docs)
print(f"Created {len(splits)} document splits.")
# 5. Create Embeddings
print("Initializing Gemini embedding model...")
embeddings = GoogleGenerativeAIEmbeddings(model=GEMINI_EMBEDDING_MODEL)
# 6. Create FAISS Vector Store
print("Creating FAISS vector store... (This may take a moment)")
# This creates the vector store in memory from the splits
db = FAISS.from_documents(splits, embeddings)
# 7. Save the Vector Store to Disk
# This saves the index locally in Colab so our Streamlit app can use it
db.save_local(VECTORSTORE_PATH)
print(f"Vector store created and saved to {VECTORSTORE_PATH}")
print("Section 2.2 complete.")
# --- 2.3 Build & Test RAG Chain ---
print("Building the RAG chain...")
# 1. Initialize the LLM
llm = ChatGoogleGenerativeAI(model=GEMINI_LLM_MODEL)
# 2. Create the Custom Prompt
# (Using the new ChatPromptTemplate)
prompt = ChatPromptTemplate.from_template(RAG_PROMPT_TEMPLATE)
# 3. Create the Document Chain
# This chain is responsible for "stuffing" the retrieved documents
# into the prompt template.
document_chain = create_stuff_documents_chain(llm, prompt)
# 4. Create the Retriever
# This object knows how to "retrieve" documents from our FAISS database
# We must initialize the embedding model to load the DB
embeddings = GoogleGenerativeAIEmbeddings(model=GEMINI_EMBEDDING_MODEL)
db = FAISS.load_local(VECTORSTORE_PATH, embeddings, allow_dangerous_deserialization=True)
retriever = db.as_retriever()
# 5. Create the Final Retrieval Chain
# This master chain links everything:
# 1. Takes the user's `input`
# 2. Sends it to the `retriever` to get documents
# 3. Sends the documents and `input` to the `document_chain`
rag_chain = create_retrieval_chain(retriever, document_chain)
print("RAG chain built successfully.")
# --- Test the chain ---
print("\n--- Running Test Query ---")
test_question = "What is the policy on workplace harassment?"
response = rag_chain.invoke({"input": test_question})
print(f"Question: {test_question}")
print(f"Answer: {textwrap.fill(response['answer'], width=80)}")
print("\n--- Running 'Golden Thread' Test Query ---")
test_question_2 = "How do I start the hiring process?"
response_2 = rag_chain.invoke({"input": test_question_2})
print(f"Question: {test_question_2}")
print(f"Answer: {textwrap.fill(response_2['answer'], width=80)}")
print("\nSection 2.3 complete.")
This section uses writefile "magic" to write the requirements.txt and the main app.py files directly into your cloned GitHub repo folder (/content/BramptonHR).
This file tells Streamlit Community Cloud which libraries to install.
%%writefile {REPO_PATH}/requirements.txt
streamlit==1.*
langchain==0.3.27
langchain-core==0.3.79
langchain-community==0.3.27
langchain-text-splitters==0.3.11
langchain-google-genai==2.1.5
faiss-cpu
pypdf
beautifulsoup4
This cell writes the main Streamlit application script.
%%writefile {REPO_PATH}/app.py
import os
import streamlit as st
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
import time
# --- 1. Global Configurations (Mirrored from Colab) ---
# We get the API key from Streamlit's secrets
# You must set this in your app's settings on share.streamlit.io
GOOGLE_API_KEY = st.secrets.get("GEMINI_API_KEY")
if not GOOGLE_API_KEY:
st.error("GEMINI_API_KEY secret not found. Please set it in your Streamlit app settings.")
st.stop()
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
# Use the exact model names that worked in Colab
GEMINI_EMBEDDING_MODEL = "models/text-embedding-004"
GEMINI_LLM_MODEL = "models/gemini-2.5-pro" # Using the model you confirmed
APP_TITLE = "Brampton L&D Co-Pilot (PoC)"
# Paths for documents and vector store
DOCS_DIR = "docs"
VECTORSTORE_PATH = "vectorstore" # This app will load the pre-built store
RAG_PROMPT_TEMPLATE = """
SYSTEM INSTRUCTION:
You are the "AI Mentor," a helpful and safe AI assistant for City of Brampton employees.
Your job is to answer questions *only* based on the official, approved policy documents provided as context.
DO NOT, under any circumstances, use any outside knowledge or make up information.
Your answer MUST follow these rules:
1. **Source Restriction:** Base your answer *only* on the text provided in the "<context>" section.
2. **No Outside Knowledge:** If the answer is not in the context, you MUST state: "I'm sorry, I do not have access to that specific information in the approved policy documents. Please contact your HR Advisor."
3. **Proactive "Golden Thread":** If (and *only* if) the user's question is about "hiring," "recruiting," or "job posting," you MUST first answer their question, and *then* you MUST add the following proactive nudge:
"Additionally, please be aware that Policy HRM-160 requires all hiring committee members to complete the 'Mandatory Recruitment and Diversity Learning Series.' You can find this module on the L&D portal."
4. **Tone:** Be professional, helpful, and concise.
<context>
{context}
</context>
Question: {input}
HELPFUL ANSWER (Based *only* on the context):
"""
# --- 2. Caching & RAG Chain Functions ---
# Use Streamlit's cache to load models and data only once
@st.cache_resource
def load_models():
"""Loads the embedding model and LLM."""
try:
embeddings = GoogleGenerativeAIEmbeddings(model=GEMINI_EMBEDDING_MODEL)
llm = ChatGoogleGenerativeAI(model=GEMINI_LLM_MODEL)
return embeddings, llm
except Exception as e:
st.error(f"Error loading AI models: {e}")
st.stop()
@st.cache_resource
def load_vector_store(_embeddings):
"""
Loads the pre-built FAISS vector store from the repo.
"""
if not os.path.exists(VECTORSTORE_PATH):
st.error(f"Vector store not found at {VECTORSTORE_PATH}. Please ensure the 'vectorstore' directory is in the GitHub repo.")
st.stop()
try:
# Load the pre-built index from local disk
db = FAISS.load_local(VECTORSTORE_PATH, _embeddings, allow_dangerous_deserialization=True)
return db
except Exception as e:
st.error(f"Error loading vector store: {e}")
st.stop()
@st.cache_resource
def create_rag_chain(_llm, _retriever):
"""Creates the final RAG chain."""
prompt = ChatPromptTemplate.from_template(RAG_PROMPT_TEMPLATE)
document_chain = create_stuff_documents_chain(_llm, prompt)
rag_chain = create_retrieval_chain(_retriever, document_chain)
return rag_chain
# --- 3. Streamlit App UI ---
st.set_page_config(page_title=APP_TITLE, layout="wide")
st.title(f"🤖 {APP_TITLE}")
st.markdown("This prototype is a demonstration for the **City of Brampton AI PoC Program**. It is an 'AI Mentor' that answers questions based *only* on the 6 official, public-facing HR policy documents.")
# Load models and data
embeddings, llm = load_models()
db = load_vector_store(embeddings)
retriever = db.as_retriever()
rag_chain = create_rag_chain(llm, retriever)
# Initialize chat history
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "assistant", "content": "Welcome! I am the AI Mentor. How can I help you with Brampton's HR policies today?"}
]
# Display chat messages from history on app rerun
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# React to user input
if prompt := st.chat_input("Ask a question (e.g., 'What is the policy on harassment?')"):
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": prompt})
# Display user message in chat message container
with st.chat_message("user"):
st.markdown(prompt)
# Display AI response in chat message container
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
response = rag_chain.invoke({"input": prompt})
full_response = response['answer']
st.markdown(full_response)
# Add AI response to chat history
st.session_state.messages.append({"role": "assistant", "content": full_response})
This is the final step. This cell will:
# --- 3.3 Deploy to GitHub ---
print("Moving vector store into the repo directory...")
# We must move the vector store *into* the repo folder so Git can commit it.
# This ensures our Streamlit app can load the pre-built index.
!mv {VECTORSTORE_PATH} {REPO_PATH}/
print("Committing and pushing all files to GitHub...")
# Use 'cd' to run Git commands from within the repo directory
git_commands = [
f"cd {REPO_PATH}",
"git add .",
f"git commit -m '{GIT_COMMIT_MESSAGE}'",
"git push"
]
!{" && ".join(git_commands)}
print("\n--- DEPLOYMENT INITIATED ---")
print(f"Pushed all files to {GITHUB_USERNAME}/{GITHUB_REPO_NAME}.")
print("Streamlit Community Cloud is now rebuilding your app.")
print("Your prototype should be live in 2-3 minutes.")
print("\n--- FINAL STEP (Manual) ---")
print("Go to your app on share.streamlit.io, go to 'Settings' > 'Secrets' and add:")
print(f"Name: GEMINI_API_KEY")
print(f"Value: [Your Google AI Studio API Key]")
print("\nSection 3.0 complete.")