Build a Gemini File Search Chatbot

Build a terminal chatbot that answers questions from your notes with Gemini.

Introduction

30 Second Summary

Important details get buried across notes you saved weeks ago. Finding one fact can mean opening file after file.

In this project, you will build a Terminal chatbot that answers questions from your own Markdown notes. Gemini File Search will ground each answer in relevant passages from those notes.

What You'll Build

Your Terminal will show a note-grounded answer with its cited source when you ask about a private fact.

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

  • A personal notes chatbot that retrieves the correct private fact from your indexed notes.
  • An evidence-only fallback that tells you when your notes do not support an answer.
  • Visible file citations beneath answers when Gemini returns citation annotations.
  • Secret Mission: Rebuild the knowledge base after adding or editing local notes. Confirm that the chatbot retrieves the new fact.

Are there any prerequisites?

You'll need basic Python familiarity plus confidence using the Terminal. You'll also need Python 3.10+ plus a Gemini API key.

Before We Start

This first checkpoint locks in the terminal chatbot you are building before any hands-on work begins. Naming the Markdown or text notes you want to question gives the project a purpose tied to information you actually use.

Set Up the Gemini Python Project

Your chatbot will soon call the Gemini API from Python. That call needs a compatible SDK plus a securely loaded credential.

A virtual environment keeps this project's packages separate from your other Python work. A session-scoped environment variable keeps the API key out of files you may share.

In this step, you will install version 2.33.0 of the Google Gen AI SDK. You will also prepare the project folder in Visual Studio Code.

In this step, get ready to:
  • Create an isolated project environment with Python 3.10 or newer.
  • Install the pinned Google Gen AI SDK.
  • Load the Gemini API key for the current Terminal session.
Build the isolated project home

The project needs its own folder before it can have an isolated environment. You will place that folder on your Desktop so it stays easy to find.

  • Use Spotlight on your Mac to open Terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The cd command changes Terminal's current location. The next command creates your project on the Desktop.

  • Create your project folder and move into it by running these commands:
mkdir personal-notes-chatbot
cd personal-notes-chatbot

What do these commands do?

  • The first command creates the personal-notes-chatbot folder on your Desktop.
  • The second command moves Terminal into that folder.
  • Check your installed Python version by running this command:
python3 --version

Why check the Python version?

The Google Gen AI SDK requires Python 3.10 or newer. This check prevents an older interpreter from causing installation problems.

✔️ I see a supported Python version

If the version is 3.10 or newer, your interpreter supports this project. Continue with the environment setup below.

ⓧ I see an older version

Your current interpreter is below the project's minimum version. The official macOS installer can add a newer Python installation without removing Apple's system-managed copy.

  • Open the official Python releases for macOS.
  • Download the macOS installer for the most recent stable Python release.
  • Open the downloaded installer package.
  • Complete the standard installation steps.
  • Return to Terminal after the installation finishes.
  • Repeat the version check shown above.

Continue when Terminal reports Python 3.10 or newer.

ⓧ Command not found

Terminal cannot find a Python 3 installation. Install a supported release from the official macOS downloads page.

  • Open the official Python releases for macOS.
  • Download the macOS installer for the most recent stable Python release.
  • Open the downloaded installer package.
  • Complete the standard installation steps.
  • Return to Terminal after the installation finishes.
  • Repeat the version check shown above.

Continue when Terminal reports Python 3.10 or newer.

The next commands create a private package space inside personal-notes-chatbot. Activating it makes the environment's Python interpreter the default for this Terminal session.

  • Return to the Terminal window inside personal-notes-chatbot.
  • Create the virtual environment and activate it by running these commands:
python3 -m venv .venv
source .venv/bin/activate

How does the environment work?

  • The first command creates an isolated Python environment inside .venv.
  • The second command activates that environment for the current Terminal session.
  • Packages installed with python now stay inside this project.
  • Confirm that your Terminal prompt shows the active environment name.

You should see the environment name at the start of the prompt. That visible prefix confirms the isolated interpreter is active.

Environment missing from the prompt?

Make sure Terminal is inside personal-notes-chatbot. Confirm that the environment creation command finished without an error.

Run the activation command again if the environment exists. Help me activate the virtual environment in my macOS Terminal.

Prepare the Python workspace

The SDK gives Python access to Gemini models plus File Search resources. Pinning version 2.33.0 keeps the project aligned with the API used in the later steps.

  • Install the pinned SDK inside the active environment by running this command:
python -m pip install google-genai==2.33.0

Why install through Python?

Using python -m pip ties the package installation to the active interpreter. The exact version pin prevents an unexpected SDK release from changing the project behavior.

  • Check the final lines of the installation output.

You should see the package name with version 2.33.0. The command should finish without an installation error.

SDK installation failed?

Check that the virtual environment is still active. Confirm that the earlier version check reported Python 3.10 or newer.

Help me debug the Google Gen AI SDK installation.

Opening the folder as a VS Code workspace keeps every project file in one place. The Explorer sidebar also gives you a visible confirmation that the correct folder is open.

  • Use Spotlight on your Mac to open Visual Studio Code.
  • Click File in the menu bar.
  • Click Open Folder....
  • Select the personal-notes-chatbot folder on your Desktop.
  • Click Open.
  • Click Yes, I trust the authors if the Workspace Trust dialog appears.

You should see personal-notes-chatbot at the top of the Explorer sidebar. The folder contains the hidden .venv environment.

Keep the credential session-scoped

Your Gemini API key grants access to your API project. Keeping it in the current Terminal session prevents the credential from being written into the Python files you will build.

Pasting a credential into Terminal can feel risky. This command stores the key in memory for the active session without printing its value.

  • Switch back to the Terminal window with the active virtual environment.
  • Replace <YOUR_API_KEY_HERE> with your Gemini API key before running this command:
export GEMINI_API_KEY=<YOUR_API_KEY_HERE>

How is the key protected?

The export command makes GEMINI_API_KEY available to programs started from this Terminal session. The Google Gen AI SDK detects that environment variable automatically.

Closing this Terminal session removes the exported value from memory. You will need to export the key again when you return to the project later.

  • Confirm that Terminal returns to the prompt without printing your key.

The empty output is expected because the command stores the value silently. Your credential remains hidden from the project files.

Before you run the final check, do you expect both the SDK import and the key lookup to succeed?

  • Verify the SDK import plus the environment variable by running this command:
python -c "import os; from google import genai; print('SDK ready:', bool(genai)); print('Key loaded:', bool(os.getenv('GEMINI_API_KEY')))"

What does the verification check?

  • The import confirms that the active environment can load the installed SDK.
  • The environment lookup checks whether GEMINI_API_KEY contains a value.
  • The command converts both checks into safe Boolean results without exposing the credential.

You should see SDK ready: True plus Key loaded: True. That's the setup complete: Python can reach the SDK while your key stays out of the source code.

One of the checks shows False?

If the SDK check fails, activate .venv again. Repeat the pinned installation in that same Terminal window.

If the key check fails, repeat the export command in the same Terminal session. Help me debug the final Gemini project setup check.

  • Crop or scroll the Terminal view so your export command is not visible before taking a screenshot.

Your isolated Gemini workspace is ready for code. Next, you will make the first model call and expose why a general chatbot cannot read a private note on your Mac.

Meet the Ungrounded Chatbot

Your isolated Python environment is ready. Its Gemini API key is loaded for this Terminal session.

A private codename in a local note creates a clear access test. You will ask for it through the Google Gen AI SDK before passing any note content to the model.

In this step, get ready to:
  • Create a local Markdown note containing a private project codename.
  • Build a one-shot Gemini request that asks for the codename.
  • Run the script to test whether Gemini can access the local note.
Prepare the local test note

The local note is your control sample. Its codename exists only on your Mac.

  • In the VS Code Explorer sidebar, create a folder named notes inside the open personal-notes-chatbot folder.
  • Inside notes, create a file named project.md using the Explorer sidebar.

You should see project.md nested beneath notes in the Explorer sidebar.

  • Paste the following content into notes/project.md:
# AI Portfolio Project

Private codename: Quokka Orbit 47

Goal: Build three small Python AI tools that can be demonstrated in a portfolio.

Practice schedule: Tuesday and Thursday at 7:00 PM.

Current project: A chatbot that answers questions from personal notes.

Why use a private codename?

  • The value Quokka Orbit 47 acts as evidence that could only come from your local note.
  • The other facts give you realistic questions for later tests.
  • Save notes/project.md from the VS Code menu bar.
  • Select notes/project.md in the Explorer sidebar to verify its saved contents.

You now have a clean test. The codename exists only in your local note.

Can't find project.md in notes?

  • Drag project.md into notes in the Explorer sidebar if both appear at the same level.
  • Compare the filename with project.md to remove any extra extension.

Help me fix the location of my note file in VS Code.

Build the one-shot model call

The Interactions API sends your question to one model invocation. The store=False setting prevents that Interaction from being retained for conversation history.

  • In the VS Code Explorer sidebar, create chat.py inside the open personal-notes-chatbot folder.

You should see chat.py beside the notes folder in the Explorer sidebar.

  • Paste the one-shot Gemini request into chat.py:
from google import genai

client = genai.Client()
interaction = client.interactions.create(
    model="gemini-3.8-flash",
    input="What is my private project codename?",
    store=False,
)
print(interaction.output_text)

What does this code do?

  • The genai.Client() call creates the connection that automatically uses your loaded API key.
  • The client.interactions.create() call sends one question to gemini-3.8-flash.
  • The store=False value keeps the Interaction stateless.
  • The interaction.output_text property provides the model's text response.
  • Save chat.py from the VS Code menu bar.
Test Gemini's access to the note

The request is ready to test. Before you run it, consider whether Gemini can recover a fact that exists only in notes/project.md.

  • Run the chatbot from the activated Terminal session by running this command:
python chat.py

You'll see a successful Gemini response. The response does not recover the exact note-backed codename Quokka Orbit 47 from notes/project.md.

That shortfall is intentional. Your successful response proves the SDK connection works.

The missing codename gives you a precise gap to solve. Gemini has no route to your local note.

Why can't Gemini read the note?

The API receives the question passed through input. A file sitting in notes remains outside that request.

A retrieval step must provide relevant note content to the model. Your current script has no retrieval step.

No model response?

  • Return to the activated Terminal session from the previous step.
  • Confirm the Terminal prompt begins with (.venv).
  • Set GEMINI_API_KEY again using the secure command from Step 1 if you opened a new Terminal session.
  • Compare chat.py with the full reference below.

Help me troubleshoot why chat.py is not returning a Gemini response.

✔️ Awesome, I've got everything!

Your two files now match the designed ungrounded version.

ⓧ I'd like to double check the full code

  • Compare your saved files with these references.
# AI Portfolio Project

Private codename: Quokka Orbit 47

Goal: Build three small Python AI tools that can be demonstrated in a portfolio.

Practice schedule: Tuesday and Thursday at 7:00 PM.

Current project: A chatbot that answers questions from personal notes.
from google import genai

client = genai.Client()
interaction = client.interactions.create(
    model="gemini-3.8-flash",
    input="What is my private project codename?",
    store=False,
)
print(interaction.output_text)

The knowledge gap is now visible. Next, you'll make the local note available for semantic search.

Index Your Notes for Semantic Search

Your first chatbot reached the Gemini API successfully. Its answer exposed the missing link because the private codename remains inside notes/project.md on your Mac.

Gemini File Search fixes that gap by indexing your local note in a persistent store. It turns note chunks into embeddings that semantic search can retrieve later.

In this step, get ready to:
  • Find supported Markdown files beneath notes/.
  • Create a persistent File Search store whose resource name is recorded locally.
  • Index each note through a polled upload operation.

Why File Search instead of sending every note?

Sending every note with every question repeats the same content. The prompt also grows with your note collection.

File Search provides a managed retrieval-augmented generation workflow. It handles chunking plus retrieval while your script focuses on the notes you want to search.

Define note discovery and operation polling

The Google Gen AI SDK returns an operation when a note upload begins. A polling helper checks that operation every five seconds until indexing finishes.

  • Use the file sidebar in VS Code to create index_notes.py directly beside chat.py.
  • Build the indexer's foundation by pasting this code into index_notes.py:
from pathlib import Path
import time

from google import genai

NOTES_DIR = Path("notes")
STORE_FILE = Path(".store_name")
SUPPORTED_SUFFIXES = {".md", ".txt"}


def wait_for_operation(client, operation):
    while not operation.done:
        time.sleep(5)
        operation = client.operations.get(operation)
    return operation

What does this code do?

  • The three constants identify your notes folder. They also identify the local store-name file plus the supported note formats.
  • The supported suffix set limits indexing to .md files plus .txt files.
  • The helper waits while the upload operation is still running.
  • Each call to client.operations.get(operation) retrieves the operation's latest status.
  • Save index_notes.py.
  • Confirm index_notes.py appears beside chat.py in the VS Code file sidebar.

File in the wrong folder?

Check that index_notes.py sits directly inside personal-notes-chatbot. Move it out of notes/ if it appears beneath project.md.

Help me place index_notes.py correctly.

Create the store and upload each note

A File Search store is a persistent cloud resource that remains available after the script exits. The local .store_name file records which store belongs to this project.

  • Place your cursor two blank lines below return operation.
  • Add note discovery plus store creation by pasting this code:
def main():
    if STORE_FILE.exists():
        raise SystemExit(
            "A File Search store is already configured. "
            "Run python cleanup.py before rebuilding it."
        )

    note_paths = sorted(
        path
        for path in NOTES_DIR.rglob("*")
        if path.is_file() and path.suffix.lower() in SUPPORTED_SUFFIXES
    )
    if not note_paths:
        raise SystemExit("Add at least one .md or .txt file inside notes/.")

    with genai.Client() as client:
        store = client.file_search_stores.create(
            config={
                "display_name": "personal-notes-chatbot",
                "embedding_model": "models/gemini-embedding-2",
            }
        )
        STORE_FILE.write_text(store.name, encoding="utf-8")

How does store creation work?

  • The opening guard stops the script when .store_name already exists. This protects the project from accidental duplicate stores.
  • The note search includes supported files at every level beneath notes/.
  • The script creates the personal-notes-chatbot store with models/gemini-embedding-2.
  • The returned store resource name is written to .store_name for the chatbot to use later.
  • Save index_notes.py.
  • Confirm the final visible line writes store.name into .store_name.

Seeing uneven indentation?

Check that the body of main() is indented by four spaces. The store creation lines sit inside the client block with eight spaces.

Help me fix the indentation in main().

Each note upload starts a separate indexing operation. The script waits for completion before it reports that note as done.

  • Place your cursor one blank line below STORE_FILE.write_text(store.name, encoding="utf-8").
  • Complete the upload workflow by pasting this code:
        try:
            for note_path in note_paths:
                print(f"Indexing {note_path.name}...", end="", flush=True)
                operation = client.file_search_stores.upload_to_file_search_store(
                    file=str(note_path),
                    file_search_store_name=store.name,
                    config={"display_name": note_path.name},
                )
                wait_for_operation(client, operation)
                print(" done")
        except Exception:
            client.file_search_stores.delete(
                name=store.name,
                config={"force": True},
            )
            STORE_FILE.unlink(missing_ok=True)
            raise

    print(f"Indexed {len(note_paths)} note(s) into {store.name}")


if __name__ == "__main__":
    main()

What happens during upload?

  • The loop uploads every supported note discovered earlier.
  • The operation polling keeps each upload active until indexing finishes.
  • The failure path force-deletes the newly created store. It also removes the unusable local marker.
  • The final message reports the indexed note count plus the created store resource name.
  • Save index_notes.py.
  • Confirm the file ends with the guard that calls main().

Upload block looks misaligned?

The try: line belongs inside the client block. The final indexed-count message returns to four spaces because it runs after the client block closes.

Help me check the upload block.

Verify the semantic index

The upload can take several five-second polling cycles. Your Terminal may remain on the indexing message while Gemini processes the note.

Before you run the indexer, what two success signals do you expect? Hold that prediction while you test it.

  • Create the File Search store by running the completed indexer once:
python index_notes.py

What should you see?

You will first see Indexing project.md... done. The final line starts with Indexed 1 note(s) into.

The rest of the final line is the File Search store resource name. You will also see .store_name inside the project folder.

That is the retrieval layer working. Your local project note now has a persistent semantic index that the chatbot can connect to next.

  • Confirm .store_name appears beside index_notes.py in the VS Code file sidebar.
  • Keep the completed File Search store in place.
  • Avoid running the indexer again during this step.

Store creation or indexing failed?

  • Confirm you are using the Terminal session that has GEMINI_API_KEY exported.
  • Check that notes/project.md still exists beneath notes/.
  • Stop if the script reports that a store is already configured. That guard means .store_name already identifies a persistent store.

Help me troubleshoot my indexing run.

✔️ Awesome, I've got everything!

Your saved indexer created one persistent store. It uploaded the project note into that store.

ⓧ I'd like to double check the full code

  • Compare your saved index_notes.py with this complete reference:
from pathlib import Path
import time

from google import genai

NOTES_DIR = Path("notes")
STORE_FILE = Path(".store_name")
SUPPORTED_SUFFIXES = {".md", ".txt"}


def wait_for_operation(client, operation):
    while not operation.done:
        time.sleep(5)
        operation = client.operations.get(operation)
    return operation


def main():
    if STORE_FILE.exists():
        raise SystemExit(
            "A File Search store is already configured. "
            "Run python cleanup.py before rebuilding it."
        )

    note_paths = sorted(
        path
        for path in NOTES_DIR.rglob("*")
        if path.is_file() and path.suffix.lower() in SUPPORTED_SUFFIXES
    )
    if not note_paths:
        raise SystemExit("Add at least one .md or .txt file inside notes/.")

    with genai.Client() as client:
        store = client.file_search_stores.create(
            config={
                "display_name": "personal-notes-chatbot",
                "embedding_model": "models/gemini-embedding-2",
            }
        )
        STORE_FILE.write_text(store.name, encoding="utf-8")

        try:
            for note_path in note_paths:
                print(f"Indexing {note_path.name}...", end="", flush=True)
                operation = client.file_search_stores.upload_to_file_search_store(
                    file=str(note_path),
                    file_search_store_name=store.name,
                    config={"display_name": note_path.name},
                )
                wait_for_operation(client, operation)
                print(" done")
        except Exception:
            client.file_search_stores.delete(
                name=store.name,
                config={"force": True},
            )
            STORE_FILE.unlink(missing_ok=True)
            raise

    print(f"Indexed {len(note_paths)} note(s) into {store.name}")


if __name__ == "__main__":
    main()
  • Confirm .store_name contains a non-empty File Search store resource name.

Your note is now searchable inside a persistent File Search store. Next, you will connect that store to the chatbot so the private codename becomes retrievable evidence.

Ground the Chatbot and Show Sources

Your note is indexed in Gemini File Search, so the private codename is now retrievable evidence. The chatbot still uses its original one-shot request.

Indexing alone does not change model behavior. This step connects retrieval to every question.

You will enforce an evidence-only answer policy. You will also surface any returned file citations.

In this step, get ready to:
  • Connect every question to the indexed File Search store.
  • Restrict answers to evidence retrieved from your notes.
  • Display unique note filenames when Gemini returns file citations.
Connect each question to File Search

The saved store resource name tells the chatbot where to retrieve evidence. The file_search tool uses that store for each question.

  • Switch back to chat.py in Visual Studio Code.
  • Press Cmd+A (macOS) or Ctrl+A (Windows) to select the existing one-shot script.
  • Replace the selected code with this first chunk:
from pathlib import Path

from google import genai

MODEL_ID = "gemini-3.8-flash"
STORE_FILE = Path(".store_name")
SYSTEM_INSTRUCTION = (
    "Answer only from information retrieved from the user's notes. "
    "If the notes do not support an answer, say: "
    "'I could not find that in your notes.' Do not guess."
)


def collect_sources(interaction):
    sources = []
    for step in interaction.steps:
        if step.type == "model_output":
            for content_block in step.content:
                if content_block.type == "text" and content_block.annotations:
                    for annotation in content_block.annotations:
                        if annotation.type == "file_citation":
                            label = annotation.file_name or "Unnamed note"
                            if label not in sources:
                                sources.append(label)
    return sources

What Does This Code Do?

  • The MODEL_ID constant keeps the model choice in one place.
  • The STORE_FILE constant points to the saved File Search store name.
  • The SYSTEM_INSTRUCTION constant limits answers to retrieved note evidence.
  • The collect_sources() function inspects model-output annotations for file citations.
  • The sources list keeps each cited filename only once.
  • Save chat.py.
  • Confirm that collect_sources() ends with return sources.

Does the source function look incomplete?

Check the indentation beneath each loop in collect_sources(). Python uses that indentation to keep the annotation checks inside the model-output traversal.

Ask for help with checking the collect_sources function indentation.

The next function sends one question through the indexed store. It returns both the answer text and the citation names collected from the response.

  • Place your cursor below return sources in chat.py.
  • Add the question function by pasting this code:
def ask_notes(client, store_name, question):
    interaction = client.interactions.create(
        model=MODEL_ID,
        input=question,
        system_instruction=SYSTEM_INSTRUCTION,
        tools=[
            {
                "type": "file_search",
                "file_search_store_names": [store_name],
            }
        ],
        store=False,
    )
    return interaction.output_text or "No answer returned.", collect_sources(interaction)

How Does the Grounded Request Work?

  • The file_search tool attaches the saved store to the request.
  • The system instruction requires evidence from the notes.
  • The store=False setting prevents the individual Interaction object from being retained for conversation history.
  • The return statement packages the answer with its unique citation names.
  • Save chat.py.
  • Confirm that file_search_store_names contains store_name.

Is the File Search tool underlined?

Check that tools contains a list with one dictionary. Make sure the closing brackets match the code above.

Ask for help with checking the File Search tool structure.

Build the interactive question loop

An interactive loop lets you ask several independent questions without restarting the script. Each question gets a fresh stateless interaction against the same indexed notes.

  • Place your cursor below the ask_notes() function in chat.py.
  • Add the interactive loop by pasting this code:
def main():
    if not STORE_FILE.exists():
        raise SystemExit("Run python index_notes.py before starting the chatbot.")

    store_name = STORE_FILE.read_text(encoding="utf-8").strip()
    print("Notes chatbot ready. Type quit to stop.")

    with genai.Client() as client:
        while True:
            question = input("\nYou: ").strip()
            if question.lower() in {"quit", "exit"}:
                print("Goodbye.")
                break
            if not question:
                continue

            answer, sources = ask_notes(client, store_name, question)
            print(f"\nBot: {answer}")
            if sources:
                print("Sources:")
                for source in sources:
                    print(f"- {source}")


if __name__ == "__main__":
    main()

What Does the Loop Control?

  • The file check stops the chatbot when .store_name is missing.
  • The saved resource name connects the local script to the persistent store.
  • The while True loop keeps accepting questions.
  • The quit and exit inputs close the loop cleanly.
  • The source block prints filenames only when citation annotations are available.
  • Save chat.py.
  • Confirm that the file ends with the call to main().

The command keeps the current Terminal occupied while the chat loop is active. The quit input closes it cleanly.

Before you run this, do you expect the chatbot to stop after one response or wait for another question? The next command tests your prediction.

  • Start the interactive loop by running this command:
python chat.py

You will see Notes chatbot ready. Type quit to stop. followed by a prompt for your first question.

  • Type quit to close this first loop check.

The Terminal prints Goodbye.. That confirms the loop can start and stop without losing the saved store.

Does the chatbot stop before showing a prompt?

Confirm that .store_name remains beside chat.py. Check that the current Terminal still has the virtual environment activated.

Ask for help with debugging the chatbot startup.

✔️ Awesome, I've got everything!

Your saved chat.py now reads the store name, retrieves note evidence, handles unsupported questions, and displays returned citation names.

ⓧ I'd like to double check the full code

  • Compare your saved chat.py with this complete file:
from pathlib import Path

from google import genai

MODEL_ID = "gemini-3.8-flash"
STORE_FILE = Path(".store_name")
SYSTEM_INSTRUCTION = (
    "Answer only from information retrieved from the user's notes. "
    "If the notes do not support an answer, say: "
    "'I could not find that in your notes.' Do not guess."
)


def collect_sources(interaction):
    sources = []
    for step in interaction.steps:
        if step.type == "model_output":
            for content_block in step.content:
                if content_block.type == "text" and content_block.annotations:
                    for annotation in content_block.annotations:
                        if annotation.type == "file_citation":
                            label = annotation.file_name or "Unnamed note"
                            if label not in sources:
                                sources.append(label)
    return sources


def ask_notes(client, store_name, question):
    interaction = client.interactions.create(
        model=MODEL_ID,
        input=question,
        system_instruction=SYSTEM_INSTRUCTION,
        tools=[
            {
                "type": "file_search",
                "file_search_store_names": [store_name],
            }
        ],
        store=False,
    )
    return interaction.output_text or "No answer returned.", collect_sources(interaction)


def main():
    if not STORE_FILE.exists():
        raise SystemExit("Run python index_notes.py before starting the chatbot.")

    store_name = STORE_FILE.read_text(encoding="utf-8").strip()
    print("Notes chatbot ready. Type quit to stop.")

    with genai.Client() as client:
        while True:
            question = input("\nYou: ").strip()
            if question.lower() in {"quit", "exit"}:
                print("Goodbye.")
                break
            if not question:
                continue

            answer, sources = ask_notes(client, store_name, question)
            print(f"\nBot: {answer}")
            if sources:
                print("Sources:")
                for source in sources:
                    print(f"- {source}")


if __name__ == "__main__":
    main()
Add a safe cleanup command

The File Search store persists after the chatbot stops. A cleanup script gives you a deliberate way to delete that indexed data later.

Deleting the store later permanently removes its indexed note data. In this step, you only create the script and keep the current store intact for the final test.

  • Create a new cleanup.py file beside chat.py using the file sidebar in Visual Studio Code.
  • Paste this cleanup code into cleanup.py:
from pathlib import Path

from google import genai

STORE_FILE = Path(".store_name")


def main():
    if not STORE_FILE.exists():
        print("No configured File Search store was found.")
        return

    store_name = STORE_FILE.read_text(encoding="utf-8").strip()
    with genai.Client() as client:
        client.file_search_stores.delete(
            name=store_name,
            config={"force": True},
        )

    STORE_FILE.unlink()
    print(f"Deleted {store_name}")


if __name__ == "__main__":
    main()

How Does Cleanup Protect the Data Lifecycle?

  • The file check makes repeated cleanup attempts safe when no configured store exists.
  • The script reads the exact resource name from .store_name.
  • The force-delete request removes the configured File Search store.
  • The local resource-name file is removed only after the store deletion succeeds.
  • Save cleanup.py.
  • Confirm that cleanup.py appears beside chat.py.
  • Keep the cleanup script unrun so the indexed store remains available for the final chatbot check.

Is cleanup.py in the wrong folder?

Move cleanup.py beside chat.py and .store_name. The script uses that shared location to find the configured store name.

Ask for help with checking the project file layout.

✔️ Awesome, I've got everything!

Your cleanup path is ready. The current File Search store remains untouched until you deliberately run cleanup.py.

ⓧ I'd like to double check the full code

  • Compare your saved cleanup.py with this complete file:
from pathlib import Path

from google import genai

STORE_FILE = Path(".store_name")


def main():
    if not STORE_FILE.exists():
        print("No configured File Search store was found.")
        return

    store_name = STORE_FILE.read_text(encoding="utf-8").strip()
    with genai.Client() as client:
        client.file_search_stores.delete(
            name=store_name,
            config={"force": True},
        )

    STORE_FILE.unlink()
    print(f"Deleted {store_name}")


if __name__ == "__main__":
    main()

Before the final run, which answer do you expect to come directly from the note? Which question should trigger the evidence-only fallback? The next check tests both predictions.

  • Start the grounded chatbot by running this command:
python chat.py

A short pause while Gemini retrieves the note is expected.

  • Enter What is my private project codename? at the first prompt.
  • Enter What is my passport number? at the next prompt.
  • Enter quit after the second answer.

You will see Quokka Orbit 47 in the first answer. That private fact proves File Search retrieved evidence from your indexed note.

The passport question produces I could not find that in your notes. as the response. The chatbot declines because the indexed note contains no supporting evidence.

When Gemini returns citation annotations, you will also see Sources: followed by project.md below the answer.

That closes the grounding loop. Your chatbot now retrieves a private fact, refuses an unsupported request, and identifies the note when citation metadata is available.

Did the grounded checks fail?

Confirm that .store_name contains the store created earlier. Check that notes/project.md still contains Quokka Orbit 47.

Ask for help with debugging the grounded chatbot response.

Secret mission

Refresh the Chatbot After Your Notes Change

Your chatbot currently searches a snapshot of your notes. Build a refresh command that replaces the old knowledge base with a new index whenever your local notes change.

Clean Up Your Resources

Clean Up Your Resources

Your local project files have no ongoing cost. The persistent Gemini File Search store is expected to cost $0 on the Gemini API Free Tier.

Paid-tier projects can incur model-token and indexing charges when you use the chatbot or rebuild the store. Choose whether to keep the store available, pause local work, or delete its indexed data.

Consider Your Note Privacy

Your indexed note data persists until you delete the File Search store. On the Free Tier, submitted content may be used to improve Google's products.

The cleanup path below targets the cloud store and its local tracking file. Your notes, Python scripts, and .venv remain on your Mac.

Resources you used:

  • One persistent Gemini File Search store containing the indexed contents of notes/project.md.
  • The local .store_name file that points the chatbot to that store.

Keep everything running

No action is needed. Choose this if you want to keep asking questions or adding more notes.

  • Continue querying the existing store by running this command in the current Terminal:
python chat.py

The chatbot reads the resource name from .store_name. Your indexed notes remain available between sessions.

Chatbot Cannot Reach the Store?

A new Terminal session needs the environment and API key setup from Step 1.

Help me troubleshoot why my personal notes chatbot cannot reach its File Search store.

Pause - I'll come back to this later

Pause local work by leaving the cloud store in place. Your indexed notes remain available when you return.

  • Stop the chatbot by entering quit at its prompt if it is running.
  • Deactivate .venv by running this command in the current Terminal:
deactivate

Your Terminal prompt should stop showing .venv. The File Search store and .store_name remain available for your next session.

Delete - I don't want to use this again

Deleting the store permanently removes its indexed data. Your local notes and Python files remain on your Mac.

  • Delete the configured store by running this command in the current Terminal:
python cleanup.py

What Does This Cleanup Do?

  • The cleanup.py script reads the store resource name from .store_name.
  • The script force-deletes the configured File Search store.
  • The script removes .store_name after the cloud deletion succeeds.

You'll see Deleted followed by the removed store's resource name.

  • Confirm that .store_name is no longer listed in the file sidebar that is already open.

Cleanup Did Not Finish?

A failed deletion leaves .store_name in place so the same store can be targeted again.

Help me troubleshoot the File Search cleanup.

Nice Work!

Nice Work!

You did it! Your Python terminal chatbot now uses the Google Gen AI SDK and Gemini File Search to answer questions from your personal notes.

You've learned how to:

  • Prove the grounding gap by asking an ungrounded model for a private fact stored only in a local note.
  • Create a persistent note index for semantic search. Connect that index to a chatbot through retrieval-augmented generation.
  • Make answers reliable with an evidence-only fallback for unsupported questions. Surface returned file citations beneath grounded answers. Remove the persistent cloud knowledge base through the cleanup workflow.
  • Secret Mission: Build a safe refresh workflow for edited notes. Verify the rebuilt chatbot knows the new demo-format fact while retaining the original codename.

Ready to quiz yourself?