Ask My Notes

Build a cited RAG chatbot that answers questions from your uploaded notes.

Introduction

30 Second Summary

Useful answers often hide across lecture notes, handouts, and personal study files. Finding one detail can mean reopening documents one by one.

In this project, you will build Ask My Notes, a browser-based study assistant that answers questions from notes you choose. Your app combines Streamlit with Gemini File Search to deliver retrieval-augmented generation with citations.

What You'll Build

Your finished app shows a grounded answer to the Project Lantern question with sample-notes.md listed beneath it as the source.

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

  • A multi-format uploader that lets you index Markdown, text, PDF, or Word notes from the browser.
  • A note-grounded chat that answers private facts from indexed files. Each response displays cited filenames plus PDF page numbers when available.
  • A persistent knowledge base that reconnects after a local restart. A deletion control removes the indexed data when you finish.
  • Secret Mission: Add a source-only guardrail that refuses questions your notes do not support.

Are there any prerequisites?

Basic Python knowledge is enough for this project. You'll need a Google account for Google AI Studio plus non-sensitive notes to test.

Before We Start

This is your moment to choose the non-sensitive notes your retrieval-grounded chatbot will answer from. Locking in a useful test case gives you a clear way to judge whether the finished app can retrieve facts from your notes.

Set Up the Free Gemini Project

Your non-sensitive notes are chosen. The chatbot still needs a dependable local foundation before it can answer a question.

This step prepares an isolated Python environment with pinned Streamlit packages. You will also verify the Xcode Command Line Tools before securing access to the Gemini Developer API.

In this step, get ready to:
  • Verify the supported Python version and macOS build tools.
  • Create an isolated project with pinned dependencies.
  • Store a Gemini API key privately without enabling paid billing.
Verify the Mac prerequisites

Streamlit 1.66.1 supports Python 3.11 through 3.15. Checking your interpreter first prevents confusing package installation failures.

The macOS Terminal runs each setup check. The build tools supply system components that Python packages may need during installation.

  • Press Command-Space bar to open Spotlight.
  • Type Terminal and press Return.
  • Check your installed Python version by running this command:
python3 --version

What does this command do?

The command asks your Mac which Python 3 interpreter it currently uses. The displayed version determines whether you can continue or need a supported release.

✔️ I see a supported version

Your Python version is ready when it falls between 3.11 and 3.15. Keep this Terminal window open for the next check.

ⓧ I see an unsupported version

A version below 3.11 or above 3.15 falls outside this project's supported range. Install the supported release before continuing.

  • Open the official Python macOS releases page.
  • Download the macOS installer for Python 3.15.1.
  • Complete the Python installer.
  • Quit Terminal after the installation finishes.
  • Reopen Terminal through Spotlight.
  • Check the newly installed version by running this command:
python3 --version

What should I see?

The output should now report Python 3.15.1. This confirms that your Mac resolves python3 to the supported installation.

ⓧ Command not found

Your shell cannot currently find Python 3. Installing the supported macOS release adds the interpreter you need.

  • Open the official Python macOS releases page.
  • Download the macOS installer for Python 3.15.1.
  • Complete the Python installer.
  • Quit Terminal after the installation finishes.
  • Reopen Terminal through Spotlight.
  • Confirm the installation by running this command:
python3 --version

What should I see?

The output should report Python 3.15.1. Your shell can now find the supported interpreter.

Still seeing the wrong Python version?

Quit every Terminal window after installing Python. Reopen Terminal through Spotlight so the shell loads the updated command path.

If the old version remains, use the installer again. Confirm that the installation finishes before reopening Terminal.

Help me diagnose why my Mac still reports an unsupported Python version.

Python packages can rely on compilers or system headers even when the project contains only Python files. The next check confirms that Apple's command-line build tools are available.

  • Check for the standalone Xcode Command Line Tools by running this command:
pkgutil --pkg-info=com.apple.pkg.CLTools_Executables

What does this command do?

The command asks macOS for the installed package information. Package details confirm that the standalone build tools are present.

✔️ Package information appears

Your Xcode Command Line Tools are installed. The package information proves that your Mac has the required build support.

ⓧ No package information appears

Your Mac needs the command-line build tools. The installer opens a macOS dialog after you run the command below.

  • Request the Xcode Command Line Tools installer by running this command:
xcode-select --install

What happens next?

macOS opens an installation dialog for the command-line tools. The installation may take several minutes while the required components download.

  • Click Install in the macOS dialog.
  • Accept the displayed license.
  • Click Done after the installation finishes.
  • Confirm the installed package by running the original check again:
pkgutil --pkg-info=com.apple.pkg.CLTools_Executables

What should I see?

Package information should now appear in Terminal. This confirms that the build tools are ready.

Installer does not open?

Check whether another macOS installation dialog is already open. Finish or close that dialog before trying the command again.

Help me troubleshoot the Xcode Command Line Tools installation.

Create the project and install dependencies

A virtual environment keeps this project's packages separate from other Python projects. Pinned versions also make your setup match the code you will build later.

You will keep the project in an ask-my-notes folder on your Desktop. Visual Studio Code keeps the files and terminal in one workspace.

  • Press Command-Space bar to open Spotlight.
  • Type Visual Studio Code and press Return.
  • Click File in the top menu bar.
  • Select Open Folder....
  • Select your Desktop folder.
  • Select New Folder.
  • Enter ask-my-notes and press Return.
  • Select Open.

You should see ask-my-notes in the Explorer view. VS Code now treats that folder as your project workspace.

  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

The dependency file records the exact package versions used by the project. Installing from this file makes the environment reproducible.

  • Select the Explorer view in the Activity Bar.
  • Select New File....
  • Enter requirements.txt and press Return.
  • Add the pinned project dependencies by pasting this content:
streamlit==1.66.1
google-genai==2.33.0

What does this file do?

  • The Streamlit pin supplies the browser interface used by the chatbot.
  • The Google Gen AI SDK pin supplies the Python client used to call Gemini.
  • The exact pins prevent package updates from changing the project while you build it.
  • Save requirements.txt with Command-S.
  • Confirm that requirements.txt appears under ask-my-notes in the Explorer view.

Dependency file missing?

Confirm that the Explorer heading shows ask-my-notes. Creating the file under another heading saves it outside this project.

Help me place requirements.txt in the correct VS Code folder.

✔️ Awesome, I've got everything!

Your dependency file is saved. Continue to the project protection file.

ⓧ I'd like to double check the full code

streamlit==1.66.1
google-genai==2.33.0

How should I use this reference?

Compare both lines with your saved file. The package names and version pins should match exactly.

The virtual environment and saved store identifier belong to your local machine. A .gitignore file prevents those local artifacts from entering source control later.

  • Select New File... in the Explorer view.
  • Enter .gitignore and press Return.
  • Add the local project exclusions by pasting this content:
.venv/
__pycache__/
.DS_Store
.ask-my-notes-store

What does this file protect?

  • The .venv/ entry excludes the project-specific Python environment.
  • The __pycache__/ entry excludes generated Python cache files.
  • The .DS_Store entry excludes macOS folder metadata.
  • The .ask-my-notes-store entry excludes the local cloud store identifier.
  • Save .gitignore with Command-S.
  • Confirm that .gitignore appears under ask-my-notes in the Explorer view.

Hidden file not visible?

Confirm that the filename starts with one period. VS Code displays dotfiles in the Explorer view after you save them.

Help me create the .gitignore file in my VS Code project.

✔️ Awesome, I've got everything!

Your project exclusions are saved. The local environment can now be created safely.

ⓧ I'd like to double check the full code

.venv/
__pycache__/
.DS_Store
.ask-my-notes-store

How should I use this reference?

Compare all four lines with your saved file. Each local artifact should appear on its own line.

The integrated terminal starts inside the open project folder. Commands run there affect ask-my-notes without requiring a separate folder path.

  • Click View in the top menu bar.
  • Select Terminal.
  • Create the isolated virtual environment by running this command:
python3 -m venv .venv

What does this command do?

Python creates a .venv folder inside ask-my-notes. Packages installed after activation stay inside this project.

  • Confirm that .venv appears in the Explorer view.

Virtual environment not created?

Check that the terminal prompt is inside the ask-my-notes workspace. Confirm that your Python version is still within the supported range.

Help me troubleshoot why Python did not create .venv.

  • Activate the virtual environment by running this command:
source .venv/bin/activate

What should I see?

Your terminal prompt should begin with (.venv). That prefix confirms that future Python commands use the isolated environment.

  • Install the pinned dependencies by running this command:
python3 -m pip install -r requirements.txt

What does this command install?

The installer reads requirements.txt inside the current folder. It installs Streamlit 1.66.1 plus Google Gen AI SDK 2.33.0 into .venv.

The first install can take a few minutes while Python downloads each package. A completed terminal prompt confirms that the installer has finished.

Package installation failed?

Confirm that the terminal prompt begins with (.venv). Check your internet connection before running the installation command again.

Help me troubleshoot the pinned dependency installation.

Create and protect the Gemini API key

Google AI Studio manages the API key used for API authentication. The project stays free while you remain within the Gemini Free Tier limits.

Why use the Gemini managed path?

Gemini File Search handles the retrieval infrastructure used later in the project. This keeps your build focused on how retrieval changes an answer.

The Free Tier covers the model and File Search usage for this project. Leave paid billing disabled.

This next action handles a live credential. Keep the key private throughout the setup.

  • Open Google's Gemini API key guide.
  • Sign in to Google AI Studio with your Google account.
  • Accept the displayed terms if Google AI Studio requests them.

New users usually receive a default project with an API key after accepting the terms. Returning users can create a key from the API Keys page.

✔️ A key is already listed

  • Open API Keys in Google AI Studio.
  • Copy the existing API key.
  • Leave paid billing disabled.

ⓧ I need to create a key

  • Open API Keys in Google AI Studio.
  • Select Create API key.
  • Choose your default project for the key.
  • Copy the new API key.
  • Leave paid billing disabled.

Keep this key private

Never paste the key into app.py. Never commit it to source control.

The Google Gen AI SDK reads GEMINI_API_KEY from your shell environment. Your code can authenticate without containing the secret.

  • Switch back to the VS Code terminal from earlier.
  • Create your Zsh configuration file if needed by running this command:
touch ~/.zshrc && open ~/.zshrc

What does this command do?

The first command ensures that ~/.zshrc exists. The second command opens that file in your Mac's default text editor.

  • Replace your-api-key with the key you copied.
  • Add the completed line to ~/.zshrc using this format:
export GEMINI_API_KEY='your-api-key'

What does this line do?

The export makes GEMINI_API_KEY available to programs launched from your Zsh shell. Quotes keep the credential together as one value.

  • Save ~/.zshrc.
  • Close the text editor.
  • Switch back to the VS Code terminal.
  • Load the saved shell configuration by running this command:
source ~/.zshrc

What should I see?

The terminal should return to the prompt without displaying your key. Your current shell can now provide the credential to the Google Gen AI SDK.

Shell configuration shows an error?

Check that the export line uses matching single quotes. Confirm that the variable name is exactly GEMINI_API_KEY.

Do not paste your real key into a help request. Redact the credential before sharing any shell configuration.

Help me fix my GEMINI_API_KEY shell configuration without exposing the key.

Your final check starts a real Streamlit example inside the activated environment. Before you run it, do you expect the result to appear in Terminal or in your browser?

  • Start the Streamlit example app by running this command:
python -m streamlit hello

What should I see?

A Streamlit example app should open in your browser. The page proves that the pinned package is installed inside the active virtual environment.

Streamlit page did not open?

Confirm that the terminal prompt begins with (.venv). Look in the terminal for the local address if your browser does not open automatically.

Help me troubleshoot why the Streamlit hello app did not open.

  • Return to the VS Code terminal.
  • Press Control+C to stop the Streamlit example.

That is the foundation in place. Your isolated environment can run Streamlit and authenticate with Gemini without exposing the API key.

Next, you will turn this setup into a working chat interface. You will also discover what happens when a general model is asked about notes it has never read.

Build a Chatbot That Cannot Read Your Notes Yet

Your Python environment can now run Streamlit. It can also authenticate with Gemini.

The Google Gen AI SDK will turn a browser prompt into a model response. The API receives only the text your app sends.

A Markdown file beside app.py stays outside that request. You will prove this gap by testing a known private fact.

In this step, get ready to:
  • Create a controlled note containing private Project Lantern facts.
  • Build a browser chat interface that keeps its message history.
  • Compare a general answer with an unsupported private-note answer.
Create a private test note

A controlled note gives you facts whose correct answers are already known. Project Lantern provides that baseline for the chatbot test.

  • Create sample-notes.md inside the ask-my-notes folder from the VS Code sidebar.
  • Add the Project Lantern note by pasting the code below:
# Project Lantern Study Plan

Project Lantern is my six-week plan for learning Python app development.

## Three study rules

1. Build one visible feature before reading more theory.
2. Test every feature with a question whose answer is already known.
3. End each session by writing the next smallest action.

## Weekly rhythm

I study on Tuesday, Thursday, and Saturday. Saturday is reserved for combining the week's features into one demo.

What does this note contain?

  • The opening identifies Project Lantern as a six-week Python app development plan.
  • The Three study rules section provides the private facts you will test.
  • The weekly rhythm adds a second set of facts for future questions.
  • Save sample-notes.md.
  • Confirm sample-notes.md is listed in the VS Code sidebar.

Cannot find the note?

  • Check that sample-notes.md sits directly inside the ask-my-notes folder.
  • Check that the filename ends with .md.

Help me check why my Project Lantern note is missing from VS Code.

✔️ Awesome, I've got everything!

Your saved sample-notes.md now holds the private facts for this test.

ⓧ I'd like to double check the full code

# Project Lantern Study Plan

Project Lantern is my six-week plan for learning Python app development.

## Three study rules

1. Build one visible feature before reading more theory.
2. Test every feature with a question whose answer is already known.
3. End each session by writing the next smallest action.

## Weekly rhythm

I study on Tuesday, Thursday, and Saturday. Saturday is reserved for combining the week's features into one demo.
Build the basic chatbot

The chatbot needs a page shell before it can accept messages. Its startup code also checks your API key before creating the Gemini client.

  • Create app.py inside the ask-my-notes folder from the VS Code sidebar.
  • Add the app startup foundation by pasting the code below:
import os

import streamlit as st
from google import genai

MODEL_ID = "gemini-3.8-flash"

st.set_page_config(page_title="Ask My Notes")
st.markdown("# Ask My Notes")
st.write("Ask Gemini a question. Your local notes have not been indexed yet.")

if not os.getenv("GEMINI_API_KEY"):
    st.error("Set GEMINI_API_KEY in your shell, then restart this app.")
    st.stop()

client = genai.Client()

if "messages" not in st.session_state:
    st.session_state.messages = []

What does this startup code do?

  • The imports load operating-system utilities plus the two project libraries.
  • MODEL_ID selects gemini-3.8-flash for every chat request.
  • The page calls create the browser title plus the opening description.
  • The environment check stops the app when GEMINI_API_KEY is unavailable.
  • genai.Client() uses the key stored in your shell environment.
  • messages starts an empty chat history for the current session.
  • Save app.py.

The run command starts a local server. It keeps the Terminal session occupied until you stop it with Control+C.

  • Switch back to the Terminal session from earlier.
  • Start the app by running this command:
python -m streamlit run app.py

What does this command do?

This command asks Python to launch app.py through Streamlit. Streamlit serves the page in your browser.

  • Confirm the browser shows Ask My Notes at the top.
  • Confirm the page says your local notes have not been indexed yet.

Browser page not opening?

  • Follow the local address printed in Terminal if the browser does not open automatically.
  • Restart the server from a Terminal session that loaded ~/.zshrc if the page shows the key warning.

Help me troubleshoot why my Streamlit app is not opening.

That first browser view confirms your app shell is running. Next, Streamlit Session State will preserve messages across the reruns caused by chat submissions.

  • Add the chat history plus the message input below the existing session-state block by pasting this code:
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

prompt = st.chat_input("Ask a question")

if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

How does the chat remember messages?

  • The first loop redraws every message already stored in st.session_state.messages.
  • st.chat_input() creates the browser field that captures a question.
  • The if prompt block stores the submitted question with the user role.
  • st.chat_message() displays the submitted text inside a chat container.
  • Save app.py.
  • Refresh the browser page.

You will see an Ask a question field at the bottom of the page.

  • Type Hello into the chat field.
  • Press Enter.

You will see Hello displayed as a user message. The message remains visible after the app reruns.

Message not staying visible?

  • Check that every reference uses st.session_state.messages.
  • Check that the user message is appended before st.markdown(prompt) runs.

Help me debug why my Streamlit chat message disappears.

The chat field can now capture a question. The remaining code sends that question to Gemini before collecting the model's text response.

  • Add the assistant response directly below the existing st.markdown(prompt) line by pasting this code:
    with st.chat_message("assistant"):
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=prompt,
        )
        answer_parts = []
        for step in interaction.steps or []:
            if step.type == "model_output":
                for content_block in step.content or []:
                    if content_block.type == "text":
                        answer_parts.append(content_block.text)
        answer = "\n\n".join(answer_parts) or "No answer was returned."
        st.markdown(answer)

    st.session_state.messages.append({"role": "assistant", "content": answer})

How does the assistant respond?

  • client.interactions.create() sends the submitted prompt to the model selected by MODEL_ID.
  • The nested loops collect text from each model_output step.
  • answer_parts keeps each returned text block until the response is complete.
  • The final append stores the assistant response in the same chat history.
Compare general and private questions

A general question checks that the model connection works. A private question checks whether the model can use facts from your local note.

Before the first test, do you expect the chatbot to answer a general knowledge question?

  • Save app.py.
  • Refresh the browser page.
  • Type What is semantic search? into the chat field.
  • Press Enter.

You will see a model-generated explanation in the assistant message. Your first API-backed answer is now live in the browser.

No assistant response?

  • Confirm the Streamlit server is still running in Terminal.
  • Confirm GEMINI_API_KEY is available to the shell that started the app.
  • Inspect Terminal for a request failure if the assistant message remains empty.

Help me debug why Gemini is not answering in my Streamlit app.

Now use the fact set that exists only inside sample-notes.md. Before you ask, do you think the model can report all three private rules from that file?

  • Type What are Project Lantern's three study rules? into the chat field.
  • Press Enter.

You will see an unsupported response or a guess that lacks evidence from sample-notes.md. This shortfall is the intended result.

Why did the private question fail?

The interaction sends MODEL_ID plus prompt to Gemini. The contents of sample-notes.md never enter that request.

A local file does not become model context merely because it sits beside app.py. The next step will import the note into Gemini File Search.

✔️ Awesome, I've got everything!

Your saved app.py now matches the complete baseline chatbot for this step.

ⓧ I'd like to double check the full code

import os

import streamlit as st
from google import genai

MODEL_ID = "gemini-3.8-flash"

st.set_page_config(page_title="Ask My Notes")
st.markdown("# Ask My Notes")
st.write("Ask Gemini a question. Your local notes have not been indexed yet.")

if not os.getenv("GEMINI_API_KEY"):
    st.error("Set GEMINI_API_KEY in your shell, then restart this app.")
    st.stop()

client = genai.Client()

if "messages" not in st.session_state:
    st.session_state.messages = []

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

prompt = st.chat_input("Ask a question")

if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=prompt,
        )
        answer_parts = []
        for step in interaction.steps or []:
            if step.type == "model_output":
                for content_block in step.content or []:
                    if content_block.type == "text":
                        answer_parts.append(content_block.text)
        answer = "\n\n".join(answer_parts) or "No answer was returned."
        st.markdown(answer)

    st.session_state.messages.append({"role": "assistant", "content": answer})

Your chatbot now answers general questions while exposing its private-note blind spot. Next, you will turn the Project Lantern file into a searchable knowledge base.

Index Your Notes with Gemini File Search

Your browser chat app can already send questions to Gemini. Its direct answer call still has no path to the private facts in sample-notes.md.

Gemini File Search turns uploaded notes into searchable chunks and embeddings. This step builds that index. It keeps the direct chat call unchanged for a controlled comparison.

In this step, get ready to:
  • Persist the File Search store name across app restarts.
  • Upload supported note files into a managed search index.
  • Test the indexed app while preserving the direct chat call.
Persist the store identity

A File Search store has a generated resource name. Saving that name in .ask-my-notes-store lets the app reconnect to the same cloud index after a local restart.

  • Return to app.py in VS Code.
  • Replace the imports and MODEL_ID section at the top of the file with this expanded setup:
import os
import tempfile
import time

import streamlit as st
from google import genai

MODEL_ID = "gemini-3.8-flash"
EMBEDDING_MODEL_ID = "models/gemini-embedding-2"
STORE_RECORD = ".ask-my-notes-store"


def read_saved_store_name():
    if not os.path.exists(STORE_RECORD):
        return None

    with open(STORE_RECORD, "r", encoding="utf-8") as store_file:
        return store_file.read().strip() or None


def save_store_name(store_name):
    with open(STORE_RECORD, "w", encoding="utf-8") as store_file:
        store_file.write(store_name)


def remove_store_record():
    if os.path.exists(STORE_RECORD):
        os.remove(STORE_RECORD)

What does this code do?

  • The new imports support temporary upload files and operation polling.
  • EMBEDDING_MODEL_ID identifies the model that converts note content into searchable representations.
  • STORE_RECORD points to the local file that holds the generated cloud store name.
  • read_saved_store_name() restores a previously saved store name.
  • save_store_name() records a newly created store name.
  • remove_store_record() keeps local record cleanup in one helper.

Streamlit reruns the script after each interaction. Session State preserves the store name and indexed filenames across those reruns.

  • Replace the existing page setup through the current messages Session State block with this code:
st.set_page_config(
    page_title="Ask My Notes",
    initial_sidebar_state="expanded",
)
st.markdown("# Ask My Notes")
st.write("Upload non-sensitive notes, index them, and ask cited questions.")

if not os.getenv("GEMINI_API_KEY"):
    st.error("Set GEMINI_API_KEY in your shell, then restart this app.")
    st.stop()

client = genai.Client()

if "store_name" not in st.session_state:
    st.session_state.store_name = read_saved_store_name()
if "indexed_files" not in st.session_state:
    st.session_state.indexed_files = []
if "messages" not in st.session_state:
    st.session_state.messages = []

with st.sidebar:
    st.markdown("### Knowledge base")
    if st.session_state.store_name:
        st.write(f"Store: `{st.session_state.store_name}`")
    else:
        st.write("No File Search store yet.")

    if st.session_state.indexed_files:
        st.write("Indexed this session: " + ", ".join(st.session_state.indexed_files))

How does the saved state work?

  • The expanded page configuration keeps the knowledge-base status visible in the sidebar.
  • store_name starts with the value restored from .ask-my-notes-store.
  • indexed_files tracks filenames uploaded during the current app session.
  • The sidebar distinguishes an existing store from a project that has not created one.
  • Save app.py.
  • Return to the browser tab running Ask My Notes.

The app reruns with an expanded sidebar. You should see Knowledge base followed by No File Search store yet.

Does the app stop during its rerun?

Check that the helper functions appear above the page configuration. A missing constant or helper can stop the script before the sidebar renders.

Compare the indentation inside each function with the code above. Python uses that indentation to determine which statements belong to the function.

Help me diagnose the rerun error in my updated app setup.

Upload and index the notes

The browser uploader returns each selected file as bytes. The indexing flow copies those bytes into a temporary file because the File Search upload call needs a file path.

Keep uploads non-sensitive

Uploading personal notes can feel sensitive. Use the included sample or other non-sensitive files because Free Tier content may be used to improve Google's products.

Imported store data persists until the store is deleted. Choose material that is safe to keep in a cloud knowledge base.

  • Insert this uploader and store-creation block immediately above the existing message-history loop:
uploaded_files = st.file_uploader(
    "Upload notes",
    type=["txt", "md", "pdf", "doc", "docx"],
    accept_multiple_files=True,
    max_upload_size=100,
)

if st.button("Index notes", disabled=not uploaded_files):
    with st.spinner("Indexing notes...", show_time=True):
        if st.session_state.store_name is None:
            store = client.file_search_stores.create(
                config={
                    "display_name": "ask-my-notes",
                    "embedding_model": EMBEDDING_MODEL_ID,
                }
            )
            st.session_state.store_name = store.name
            save_store_name(store.name)

What does this first indexing slice do?

  • The uploader accepts text, Markdown, PDF, Word, and Open XML Word files.
  • max_upload_size=100 applies the File Search limit of 100 MB to each selected file.
  • The indexing button stays disabled until at least one file is selected.
  • The store configuration gives the knowledge base the display name ask-my-notes.
  • The generated store name is saved in Session State and .ask-my-notes-store.
  • Save app.py.
  • Select sample-notes.md with the Upload notes uploader.
  • Click Index notes.

The sidebar now shows a generated store name. This proves the persistent cloud store exists and its identifier has been saved locally.

The store now needs the upload loop that imports each selected note into its search index.

  • Find save_store_name(store.name) inside the indexing button block.
  • Insert this upload flow directly below that line:
        for uploaded_file in uploaded_files:
            suffix = os.path.splitext(uploaded_file.name)[1]
            with tempfile.NamedTemporaryFile(
                mode="w+b",
                suffix=suffix,
                delete=False,
            ) as temporary_file:
                temporary_file.write(uploaded_file.getvalue())
                temporary_path = temporary_file.name

            try:
                operation = client.file_search_stores.upload_to_file_search_store(
                    file=temporary_path,
                    file_search_store_name=st.session_state.store_name,
                    config={"display_name": uploaded_file.name},
                )
                while not operation.done:
                    time.sleep(5)
                    operation = client.operations.get(operation)
            finally:
                os.remove(temporary_path)

        new_names = [uploaded_file.name for uploaded_file in uploaded_files]
        st.session_state.indexed_files.extend(new_names)

    st.write("Indexed: " + ", ".join(new_names))

How does indexing work?

  • The filename extension becomes the suffix of a secure temporary file.
  • getvalue() copies the uploaded bytes into that temporary file.
  • upload_to_file_search_store() imports the temporary file into the persistent store.
  • The polling loop checks the operation every five seconds until processing finishes.
  • The finally block removes the temporary local copy even if the upload raises an error.
  • new_names updates the browser with the files indexed during this session.

Indexing can span several five-second polling cycles. The timed spinner shows that the app is still waiting for the managed import operation.

  • Save app.py.
  • Select sample-notes.md in the uploader.
  • Click Index notes.
  • Wait for the timed spinner to close.

That is the managed index working. You should see Indexed: sample-notes.md on the page. The sidebar should show the saved store name plus Indexed this session: sample-notes.md.

Does indexing fail or keep spinning?

Confirm that the running app still has access to the Gemini API key. Restarting it from a different Terminal session can remove that environment value from the process.

Check that the selected file uses one of the uploader's listed formats. Also confirm that its size stays within the displayed per-file limit.

Help me troubleshoot why my Gemini File Search upload does not finish.

Test what indexing changed

The app now has a searchable knowledge base. The chat area still uses the original direct interaction so you can test whether creating an index changes that call by itself.

  • Replace the existing message-history loop and chat-input line with this labeled chat section:
st.markdown("### Chat")

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

prompt = st.chat_input("Ask a question", submit_mode="disable")

Why update the chat section?

The heading separates questions from the indexing controls. The input setting prevents duplicate submissions while Streamlit processes a question.

The existing client.interactions.create() call remains unchanged. It still receives only the model identifier and the user's prompt.

  • Save app.py.
  • Compare your complete file with the reference below.

✔️ Awesome, I've got everything!

Great. Make sure app.py is saved before testing the indexed app.

ⓧ I'd like to double check the full code

import os
import tempfile
import time

import streamlit as st
from google import genai

MODEL_ID = "gemini-3.8-flash"
EMBEDDING_MODEL_ID = "models/gemini-embedding-2"
STORE_RECORD = ".ask-my-notes-store"


def read_saved_store_name():
    if not os.path.exists(STORE_RECORD):
        return None

    with open(STORE_RECORD, "r", encoding="utf-8") as store_file:
        return store_file.read().strip() or None


def save_store_name(store_name):
    with open(STORE_RECORD, "w", encoding="utf-8") as store_file:
        store_file.write(store_name)


def remove_store_record():
    if os.path.exists(STORE_RECORD):
        os.remove(STORE_RECORD)


st.set_page_config(
    page_title="Ask My Notes",
    initial_sidebar_state="expanded",
)
st.markdown("# Ask My Notes")
st.write("Upload non-sensitive notes, index them, and ask cited questions.")

if not os.getenv("GEMINI_API_KEY"):
    st.error("Set GEMINI_API_KEY in your shell, then restart this app.")
    st.stop()

client = genai.Client()

if "store_name" not in st.session_state:
    st.session_state.store_name = read_saved_store_name()
if "indexed_files" not in st.session_state:
    st.session_state.indexed_files = []
if "messages" not in st.session_state:
    st.session_state.messages = []

with st.sidebar:
    st.markdown("### Knowledge base")
    if st.session_state.store_name:
        st.write(f"Store: `{st.session_state.store_name}`")
    else:
        st.write("No File Search store yet.")

    if st.session_state.indexed_files:
        st.write("Indexed this session: " + ", ".join(st.session_state.indexed_files))

uploaded_files = st.file_uploader(
    "Upload notes",
    type=["txt", "md", "pdf", "doc", "docx"],
    accept_multiple_files=True,
    max_upload_size=100,
)

if st.button("Index notes", disabled=not uploaded_files):
    with st.spinner("Indexing notes...", show_time=True):
        if st.session_state.store_name is None:
            store = client.file_search_stores.create(
                config={
                    "display_name": "ask-my-notes",
                    "embedding_model": EMBEDDING_MODEL_ID,
                }
            )
            st.session_state.store_name = store.name
            save_store_name(store.name)

        for uploaded_file in uploaded_files:
            suffix = os.path.splitext(uploaded_file.name)[1]
            with tempfile.NamedTemporaryFile(
                mode="w+b",
                suffix=suffix,
                delete=False,
            ) as temporary_file:
                temporary_file.write(uploaded_file.getvalue())
                temporary_path = temporary_file.name

            try:
                operation = client.file_search_stores.upload_to_file_search_store(
                    file=temporary_path,
                    file_search_store_name=st.session_state.store_name,
                    config={"display_name": uploaded_file.name},
                )
                while not operation.done:
                    time.sleep(5)
                    operation = client.operations.get(operation)
            finally:
                os.remove(temporary_path)

        new_names = [uploaded_file.name for uploaded_file in uploaded_files]
        st.session_state.indexed_files.extend(new_names)

    st.write("Indexed: " + ", ".join(new_names))

st.markdown("### Chat")

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

prompt = st.chat_input("Ask a question", submit_mode="disable")

if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=prompt,
        )
        answer_parts = []
        for step in interaction.steps or []:
            if step.type == "model_output":
                for content_block in step.content or []:
                    if content_block.type == "text":
                        answer_parts.append(content_block.text)
        answer = "\n\n".join(answer_parts) or "No answer was returned."
        st.markdown(answer)

    st.session_state.messages.append({"role": "assistant", "content": answer})

Before you test it, do you think creating the index changed what the direct answer call can read?

  • Enter What are Project Lantern's three study rules? in the chat input.
  • Send the question.

The response remains unsupported or guessed. That shortfall is intentional. The notes are searchable, but the direct interaction has no File Search tool that can retrieve them.

Your notes are now searchable in a persistent store. Next, you'll connect that store to the answer call. You will also display the files that support each response.

Ground Answers and Show Sources

Your Streamlit app now turns uploaded notes into a persistent Gemini File Search store. The private facts are indexed.

However, the chat request still ignores that store. Connecting the stored notes to the interaction completes retrieval-augmented generation.

In this step, get ready to:
  • Connect each chat request to the saved File Search store.
  • Extract source filenames from citation annotations.
  • Add a control that deletes the indexed knowledge base.
Connect chat to File Search

The current interaction sends only the question to Gemini. The File Search tool gives that interaction access to the store recorded in .ask-my-notes-store.

  • In app.py, scroll to the client.interactions.create() call near the bottom of the file.
  • Find this block:
interaction = client.interactions.create(
    model=MODEL_ID,
    input=prompt,
)

What does the current interaction do?

This request gives Gemini the learner's question. It has no connection to the indexed notes.

  • Replace that block with this File Search interaction:
interaction = client.interactions.create(
    model=MODEL_ID,
    input=prompt,
    tools=[
        {
            "type": "file_search",
            "file_search_store_names": [
                st.session_state.store_name
            ],
        }
    ],
)

How does retrieval enter the request?

  • The file_search tool searches the indexed chunks for passages related to the question.
  • The file_search_store_names list points the tool to the store restored from .ask-my-notes-store.
  • The original prompt remains the interaction input.
  • Save app.py.
  • Switch back to the running Ask My Notes app from earlier.

Before you ask, predict whether the indexed study rules will now appear in the answer.

  • Enter What are Project Lantern's three study rules? in the chat field.
  • Press Enter.

You should see the three rules from sample-notes.md. The interaction now retrieves private facts from the indexed store.

Still seeing a guessed answer?

Confirm that the sidebar still shows a saved store name. Confirm that sample-notes.md was indexed into that store.

Compare the tool configuration with the code above. A typo in st.session_state.store_name prevents the interaction from reaching the saved store.

Ask for targeted help with the retrieval request:

Extract and preserve citations

The retrieved answer can include file citation annotations. Parsing those annotations turns the model response into evidence you can inspect.

  • In app.py, find the response parsing block that begins with answer_parts = [].
  • Locate this existing code:
answer_parts = []
for step in interaction.steps or []:
    if step.type == "model_output":
        for content_block in step.content or []:
            if content_block.type == "text":
                answer_parts.append(content_block.text)
answer = "\n\n".join(answer_parts) or "No answer was returned."

What is missing from this parser?

The current parser collects text from model output blocks. It leaves the citation annotations inside the response object.

  • Replace that parser with this citation-aware version:
answer_parts = []
sources = []

for step in interaction.steps or []:
    if step.type == "model_output":
        for content_block in step.content or []:
            if content_block.type == "text":
                answer_parts.append(content_block.text)
                for annotation in content_block.annotations or []:
                    if annotation.type == "file_citation":
                        source_label = annotation.file_name or "Unknown file"
                        page_number = getattr(
                            annotation,
                            "page_number",
                            None,
                        )
                        if page_number is not None:
                            source_label += f", page {page_number}"
                        sources.append(source_label)

answer = "\n\n".join(answer_parts) or "No answer was returned."
sources = list(dict.fromkeys(sources))

How are source labels built?

  • The parser keeps annotations whose type is file_citation.
  • Each label starts with annotation.file_name.
  • A PDF label gains page N when the annotation contains a page number.
  • The final conversion removes duplicate labels while preserving their order.
  • Find the line that displays answer below the response parser.
  • Locate this code:
st.markdown(answer)

What does the current display show?

The app renders the answer text. The source labels remain in memory until the display includes them.

  • Replace that line with this answer and source display:
st.markdown(answer)
if sources:
    st.write("Sources: " + "; ".join(sources))

What does the source display add?

The app writes one source line after the answer when citations are available. Multiple labels use a clear separator.

  • Save app.py.
  • Switch back to the running app from earlier.

Before you test the parser, predict which filename should appear below the Project Lantern answer.

  • Enter What are Project Lantern's three study rules? in the chat field.
  • Press Enter.

You should see the three study rules followed by Sources: sample-notes.md.

Answer visible but sources missing?

Check that the annotation loop is nested inside the text-block condition. Confirm that sources.append(source_label) remains inside the citation condition.

Ask for help tracing the citation data:

The visible source line proves the parser works. The message history now needs the same sources field so later reruns can restore those labels.

  • Return to the Terminal panel from earlier.
  • Stop the running app by pressing Control+C.
  • In app.py, find the chat history block that begins with st.markdown("### Chat").
  • Locate this existing region:
st.markdown("### Chat")

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

prompt = st.chat_input("Ask a question", submit_mode="disable")

if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

Why update the message shape?

Each stored message currently has only a role and content. A consistent sources field lets the history renderer handle user messages and assistant messages with one structure.

  • Replace that region with this source-aware history code:
st.markdown("### Chat")

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])
        if message["sources"]:
            st.write("Sources: " + "; ".join(message["sources"]))

prompt = st.chat_input(
    "Ask a question about your notes",
    disabled=st.session_state.store_name is None,
    submit_mode="disable",
)

if prompt:
    st.session_state.messages.append(
        {"role": "user", "content": prompt, "sources": []}
    )
    with st.chat_message("user"):
        st.markdown(prompt)

How does the history change?

  • Past assistant messages render their saved source labels during each rerun.
  • User messages carry an empty source list.
  • The chat input stays disabled until a File Search store is available.
  • Find the assistant message append at the bottom of app.py.
  • Locate this line:
st.session_state.messages.append({"role": "assistant", "content": answer})

What does the current assistant record keep?

The assistant record saves the answer text. It drops the source labels after the current run finishes.

  • Replace that line with this complete assistant record:
st.session_state.messages.append(
    {"role": "assistant", "content": answer, "sources": sources}
)

Why save sources with the answer?

Streamlit reruns the script after each interaction. Storing the source list in Session State keeps each citation attached to its answer.

  • Save app.py.
  • Restart the Ask My Notes app by running this command in the activated virtual environment:
python -m streamlit run app.py

What does this command restore?

Streamlit starts the local app again. The app reads the existing store identifier from .ask-my-notes-store.

You should see the saved File Search store name in the sidebar. The chat waits for a new question.

  • Enter What are Project Lantern's three study rules? in the chat field.
  • Press Enter.
  • Enter Which day is reserved for combining the week's features? as a second question.
  • Press Enter.

You should see both conversations in the chat. Each grounded answer keeps its source label after the second question triggers a rerun.

App fails after the history update?

Confirm that you stopped the earlier server before changing the message shape. Restarting creates a fresh Session State whose messages all include sources.

Ask for help checking the message dictionaries:

Add the deletion control and verify the app

The File Search store is a persistent cloud resource. A deletion control gives you a direct way to remove the indexed notes when you finish.

  • In app.py, return to the with st.sidebar: block.
  • Find this indexed-files display:
if st.session_state.indexed_files:
    st.write("Indexed this session: " + ", ".join(st.session_state.indexed_files))

Where does the control belong?

This display sits inside the sidebar. Placing the deletion control below it keeps knowledge-base management in one location.

  • Replace that display with this complete sidebar section:
if st.session_state.indexed_files:
    st.write("Indexed this session: " + ", ".join(st.session_state.indexed_files))

if st.button(
    "Delete indexed notes",
    disabled=st.session_state.store_name is None,
):
    client.file_search_stores.delete(
        name=st.session_state.store_name,
        config={"force": True},
    )
    remove_store_record()
    st.session_state.store_name = None
    st.session_state.indexed_files = []
    st.session_state.messages = []
    st.rerun()

What does deletion clean up?

  • The API call force-deletes the saved File Search store.
  • The remove_store_record() helper deletes the local store identifier.
  • The Session State assignments clear the store name, indexed filenames, and chat history.
  • The st.rerun() call refreshes the interface with the empty state.
  • Save app.py.
  • Switch back to the running app from earlier.
  • Confirm that the sidebar shows the Delete indexed notes button.

The button should be enabled because the app restored the existing store name. Leave it unclicked so the indexed notes remain available for the final check.

Deletion button disabled?

Confirm that the sidebar shows a store name above the button. A disabled button means st.session_state.store_name is empty.

Check that .ask-my-notes-store still contains the generated store name. Restart the app after restoring that file.

Ask for help checking the restored store state:

Use the tabs below to confirm that your final app.py matches the completed retrieval app.

✔️ Awesome, I've got everything!

Great. Save app.py before completing the final retrieval check.

ⓧ I'd like to double check the full code

import os
import tempfile
import time

import streamlit as st
from google import genai

MODEL_ID = "gemini-3.8-flash"
EMBEDDING_MODEL_ID = "models/gemini-embedding-2"
STORE_RECORD = ".ask-my-notes-store"


def read_saved_store_name():
    if not os.path.exists(STORE_RECORD):
        return None

    with open(STORE_RECORD, "r", encoding="utf-8") as store_file:
        return store_file.read().strip() or None


def save_store_name(store_name):
    with open(STORE_RECORD, "w", encoding="utf-8") as store_file:
        store_file.write(store_name)


def remove_store_record():
    if os.path.exists(STORE_RECORD):
        os.remove(STORE_RECORD)


st.set_page_config(
    page_title="Ask My Notes",
    initial_sidebar_state="expanded",
)
st.markdown("# Ask My Notes")
st.write("Upload non-sensitive notes, index them, and ask cited questions.")

if not os.getenv("GEMINI_API_KEY"):
    st.error("Set GEMINI_API_KEY in your shell, then restart this app.")
    st.stop()

client = genai.Client()

if "store_name" not in st.session_state:
    st.session_state.store_name = read_saved_store_name()
if "indexed_files" not in st.session_state:
    st.session_state.indexed_files = []
if "messages" not in st.session_state:
    st.session_state.messages = []

with st.sidebar:
    st.markdown("### Knowledge base")
    if st.session_state.store_name:
        st.write(f"Store: `{st.session_state.store_name}`")
    else:
        st.write("No File Search store yet.")

    if st.session_state.indexed_files:
        st.write("Indexed this session: " + ", ".join(st.session_state.indexed_files))

    if st.button(
        "Delete indexed notes",
        disabled=st.session_state.store_name is None,
    ):
        client.file_search_stores.delete(
            name=st.session_state.store_name,
            config={"force": True},
        )
        remove_store_record()
        st.session_state.store_name = None
        st.session_state.indexed_files = []
        st.session_state.messages = []
        st.rerun()

uploaded_files = st.file_uploader(
    "Upload notes",
    type=["txt", "md", "pdf", "doc", "docx"],
    accept_multiple_files=True,
    max_upload_size=100,
)

if st.button("Index notes", disabled=not uploaded_files):
    with st.spinner("Indexing notes...", show_time=True):
        if st.session_state.store_name is None:
            store = client.file_search_stores.create(
                config={
                    "display_name": "ask-my-notes",
                    "embedding_model": EMBEDDING_MODEL_ID,
                }
            )
            st.session_state.store_name = store.name
            save_store_name(store.name)

        for uploaded_file in uploaded_files:
            suffix = os.path.splitext(uploaded_file.name)[1]
            with tempfile.NamedTemporaryFile(
                mode="w+b",
                suffix=suffix,
                delete=False,
            ) as temporary_file:
                temporary_file.write(uploaded_file.getvalue())
                temporary_path = temporary_file.name

            try:
                operation = client.file_search_stores.upload_to_file_search_store(
                    file=temporary_path,
                    file_search_store_name=st.session_state.store_name,
                    config={"display_name": uploaded_file.name},
                )
                while not operation.done:
                    time.sleep(5)
                    operation = client.operations.get(operation)
            finally:
                os.remove(temporary_path)

        new_names = [uploaded_file.name for uploaded_file in uploaded_files]
        st.session_state.indexed_files.extend(new_names)

    st.write("Indexed: " + ", ".join(new_names))

st.markdown("### Chat")

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])
        if message["sources"]:
            st.write("Sources: " + "; ".join(message["sources"]))

prompt = st.chat_input(
    "Ask a question about your notes",
    disabled=st.session_state.store_name is None,
    submit_mode="disable",
)

if prompt:
    st.session_state.messages.append(
        {"role": "user", "content": prompt, "sources": []}
    )
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        with st.spinner("Searching your notes...", show_time=True):
            interaction = client.interactions.create(
                model=MODEL_ID,
                input=prompt,
                tools=[
                    {
                        "type": "file_search",
                        "file_search_store_names": [
                            st.session_state.store_name
                        ],
                    }
                ],
            )

            answer_parts = []
            sources = []

            for step in interaction.steps or []:
                if step.type == "model_output":
                    for content_block in step.content or []:
                        if content_block.type == "text":
                            answer_parts.append(content_block.text)
                            for annotation in content_block.annotations or []:
                                if annotation.type == "file_citation":
                                    source_label = annotation.file_name or "Unknown file"
                                    page_number = getattr(
                                        annotation,
                                        "page_number",
                                        None,
                                    )
                                    if page_number is not None:
                                        source_label += f", page {page_number}"
                                    sources.append(source_label)

            answer = "\n\n".join(answer_parts) or "No answer was returned."
            sources = list(dict.fromkeys(sources))

        st.markdown(answer)
        if sources:
            st.write("Sources: " + "; ".join(sources))

    st.session_state.messages.append(
        {"role": "assistant", "content": answer, "sources": sources}
    )

What should match?

Compare your imports, store helpers, sidebar controls, indexing flow, chat history, retrieval tool, citation parser, and message records with this reference.

Before you submit the final check, predict which answer details should come from the indexed note.

  • Enter What are Project Lantern's three study rules? in the chat field.
  • Press Enter.

You should see all three rules from sample-notes.md. You should also see Sources: sample-notes.md below the answer.

That closes the retrieval loop. Your app now searches the indexed notes, answers from relevant passages, and shows the evidence behind its response.

Secret mission

Refuse Unsupported Answers

Your cited chatbot can still answer from general knowledge when its indexed notes are silent. Add a source-only prompt that returns one exact fallback for unsupported questions while preserving grounded answers and citations.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep Ask My Notes available, pause its local server, or delete its stored data and files. This project remains at $0 while your Gemini usage stays within the Free Tier limits.

Resources you used:

  • A running Streamlit server for the local chat app.
  • A local ask-my-notes folder containing the source files and the .venv virtual environment.
  • A persistent Gemini File Search store containing the indexed note data.
  • A local .ask-my-notes-store file containing the persistent store name.
  • A Google AI Studio API key used by the app.

Keep everything running

No action is needed. Choose this option if you are still asking questions through your indexed notes.

  • Leave the Streamlit server running in Terminal.
  • Keep the File Search store available for future questions.
  • Keep .ask-my-notes-store so the app can reconnect to the saved store.
  • Keep the Gemini API key active so the app can make requests.
  • Upload only non-sensitive notes because Free Tier content may be used to improve Google's products.

Pause - I'll come back to this later

Shut down the local server to free up memory. Your project files and indexed store remain ready for your return.

  • Press Control+C in Terminal to stop the running app.
  • Keep the ask-my-notes folder on your Mac.
  • Keep the File Search store so its indexed note data persists.
  • Restart the app later from the ask-my-notes folder by running these commands:
source .venv/bin/activate
python -m streamlit run app.py

What Do These Commands Do?

The first command reactivates the existing virtual environment. The second command starts the Streamlit app.

The app reads .ask-my-notes-store during startup. This reconnects the interface to the persistent cloud store.

  • Confirm the restart by finding the saved File Search store name in the app sidebar.

App Does Not Restart?

  • Confirm that Terminal is inside the ask-my-notes folder.
  • Confirm that the .venv folder still exists inside ask-my-notes.

Help me diagnose why my Ask My Notes app does not restart.

Delete - I don't want to use this again

Deleting the File Search store permanently removes its indexed note data. Your original notes remain on your Mac until you remove the local project folder.

  • Switch back to the running Ask My Notes app.
  • Click Delete indexed notes in the sidebar.
  • Confirm that the sidebar shows No File Search store yet.
  • Return to Terminal.
  • Press Control+C to stop the app.

What Did the Delete Control Remove?

The control force-deletes the persistent File Search store containing your indexed notes. It also removes the local .ask-my-notes-store identifier file.

The app also clears the current chat history and the list of files indexed during this session.

Revoking the API key only disables this credential. Your Google account remains available.

  • Follow Google's API key guide to return to Google AI Studio.
  • Select API Keys.
  • Revoke the API key you created for Ask My Notes.

Deleting the local folder is permanent. Any personal notes stored inside ask-my-notes disappear with it.

  • Move any personal files you want to keep out of the ask-my-notes folder.
  • Remove the local project folder by running these commands:
cd ..
rm -rf ask-my-notes
ls

What Does This Delete?

The commands move Terminal above the project directory. They permanently remove the ask-my-notes folder.

The final command lists the surrounding directory so you can verify that the folder is gone.

  • Confirm the deletion by checking that ask-my-notes is absent from the output.

Still See the Project Folder?

  • Confirm that Terminal moved one level above the ask-my-notes folder.
  • Confirm that the folder name is exactly ask-my-notes.

Help me safely remove the remaining Ask My Notes folder.

Nice Work!

Nice Work!

You did it! Your browser-based Python study assistant now answers from indexed notes through Gemini File Search with visible source labels.

You've learned how to:

  • Build a browser-based Streamlit chat app that preserves each conversation through Session State. Test a private Project Lantern fact to expose the chatbot's original retrieval gap.
  • Index Markdown, text, PDF, and Word notes in a persistent Gemini File Search knowledge base. Turn each note into searchable chunks with embeddings. Reconnect to the generated store through .ask-my-notes-store after local restarts.
  • Complete a retrieval-augmented generation loop that grounds Gemini answers in indexed notes. Parse citation annotations into filenames plus PDF page numbers when available. Remove indexed cloud data through the Delete indexed notes control.
  • Secret Mission: Add a source-only guardrail that returns I couldn't find that in your notes. for unsupported questions. Verify that supported Project Lantern questions still produce cited answers from sample-notes.md.

Ready to quiz yourself?