Ask My PDF with Python
Build a Streamlit chatbot that answers uploaded PDF questions with Gemini.
Introduction
30 Second Summary
Long reports often hide the one fact you need among dozens of pages. Finding it can mean searching the same document several times.
In this project, you will build a local Python chatbot that answers questions about one public PDF. Your Streamlit interface sends the whole document to Gemini 3.8 Flash for grounded answers.
What You'll Build
Your browser becomes a document chat where each question produces an answer from the uploaded PDF while the full conversation remains visible.
By the end of this project, you'll have:
- A working PDF upload that unlocks the chat when your document is ready.
- Document-grounded answers that can use the PDF's text, tables, diagrams, or charts. Missing information produces I can't find that in the PDF.
- Visible chat history that keeps follow-up questions on screen. Failed requests appear as readable chat responses.
- Secret Mission: Add a reset control that clears old messages before you switch to another document.
Are there any prerequisites?
You'll need a Google account plus an internet connection. Choose one public or sample PDF no larger than 50 MB because free-tier content is used to improve Google's products.
Before We Start
This checkpoint commits you to a public or sample PDF no larger than 50 MB. Naming why someone would question it gives your chatbot a clear purpose before setup begins.
Set Up the Python Workspace
The public PDF you chose needs a dependable Python environment before it can power a chatbot. Version conflicts can stop the project before your document reaches the browser.
A virtual environment keeps this project's packages separate from your other Python work. Your Gemini credential will stay in the shell instead of the source code.
You will also verify Apple Command Line Tools before installing the dependencies. This gives your Mac the system foundation that Python packages may need.
In this step, get ready to:
- Verify that your Mac supports the project.
- Create an isolated workspace in Visual Studio Code.
- Prepare a runnable Streamlit environment with a Gemini API credential.
Verify your Mac prerequisites
Streamlit supports Python 3.11 through 3.15. Checking the installed version now prevents an incompatible interpreter from reaching the package installation.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal in the search bar.
- Press Enter to open macOS Terminal.
- Check your installed Python version by running this command:
python3 --version
What does this check do?
The command asks the installed python3 executable to print its version. The result shows whether that interpreter can run this project's packages.
✔️ I see version 3.11 through 3.15
Your installed interpreter is inside the supported range. Python is ready for the project.
ⓧ I see a version outside the range
Your current interpreter falls outside the supported range. Python 3.15.1 has an official macOS installer.
- Open the official Python macOS releases page.
- Download the macOS installer for Python 3.15.1.
- Complete the installer prompts.
- Return to Terminal after the installation finishes.
- Check the installed version again by running:
python3 --version
What should this confirm?
The command should now print Python 3.15.1. That version sits inside the supported range.
ⓧ Command not found
Your shell cannot find a Python 3 installation. The official macOS installer adds a supported interpreter to your Mac.
- Open the official Python macOS releases page.
- Download the macOS installer for Python 3.15.1.
- Complete the installer prompts.
- Close Terminal after the installation finishes.
- Open a fresh Terminal window through your search bar.
- Confirm that the fresh shell can find Python by running:
python3 --version
What should this confirm?
The command should print Python 3.15.1. The fresh shell can now use the installed interpreter.
Still unable to find Python?
Confirm that the Python installer completed before opening the fresh Terminal window. A shell that stayed open during installation may still have the old path.
If the command remains unavailable, help me check why macOS cannot find my Python installation.
Python packages can also rely on developer components supplied by Apple. The next check confirms that macOS has an active developer directory.
- Check the active Apple developer directory by running this command:
xcode-select --print-path
What does this check do?
The command prints the active developer directory when Apple Command Line Tools are available. A returned path confirms that the system components are ready.
✔️ I see a developer directory
Your Mac has an active Apple developer directory. The command-line components are ready.
ⓧ No active developer directory
Your Mac needs Apple Command Line Tools. The next command opens Apple's installer dialog.
- Request the Apple Command Line Tools installer by running:
xcode-select --install
What does this command do?
The command asks macOS to open the system installer. The installer downloads the developer components used by command-line software.
The download can take several minutes. A quiet progress screen during that wait is expected.
- Complete the prompts in Apple's installer dialog.
- Return to Terminal after the installation finishes.
- Confirm the active developer directory by running:
xcode-select --print-path
What should this confirm?
The command should now return a directory path. That path confirms the installation succeeded.
Installer not completing?
Allow the system installer to finish before repeating the directory check. A partial installation cannot provide an active developer path.
If the installer still fails, help me install Apple Command Line Tools on my Mac.
That foundation is now confirmed. Your Mac has a supported Python interpreter plus the Apple components needed for package installation.
Prepare an isolated workspace
The Python extension for Visual Studio Code connects the editor to your interpreter. A dedicated .venv folder keeps every installed package inside this project.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code in the search bar.
- Press Enter to open Visual Studio Code.
The editor needs Microsoft's Python support before it can create the project environment. The extension also lets VS Code select the interpreter stored inside that environment.
- Select the Extensions icon in the Activity Bar.
- Type Python in the extension search box.
- Select the Python extension published by Microsoft.
- Select Install.
The extension page replaces the install control with a management control. Python support is now active in the editor.
Why use a virtual environment?
A virtual environment gives this project its own interpreter context. Installing packages into .venv prevents another Python project from changing the versions used here.
Your project files also need one named location. You will create the ask-my-pdf folder on your Desktop.
- Select File from the top menu bar.
- Select Open Folder....
- Select your Desktop in the folder dialog.
- Select New Folder.
- Enter ask-my-pdf as the folder name.
- Select Open.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
The Explorer view should show ask-my-pdf as the open folder. Your chatbot now has a dedicated workspace.
VS Code can create the virtual environment from the interpreter you verified earlier. Choosing that interpreter keeps the project inside the supported Python range.
- Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
- Type Python: Create Environment.
- Select Python: Create Environment from the results.
- Select Venv as the environment type.
- Select the supported Python interpreter you verified earlier.
The Explorer view should now contain a .venv folder. VS Code also selects that environment for the workspace.
- Select Terminal from the top menu bar.
- Select New Terminal to create a fresh terminal inside ask-my-pdf.
- Activate the virtual environment by running this command:
source .venv/bin/activate
What does this command do?
The command updates the current shell to use the interpreter inside .venv. Future package installations now stay with this project.
Your terminal prompt should begin with (.venv). That prefix confirms the environment is active.
Virtual environment not active?
Confirm that .venv appears inside the ask-my-pdf folder. Repeat the environment workflow if the folder is absent.
Confirm that the terminal opened from the current VS Code workspace. A terminal in another folder cannot find this activation script.
If activation still fails, help me activate my virtual environment in VS Code on macOS.
Make the workspace runnable
A requirements file records the exact dependency versions used by the project. This makes the environment reproducible when you return to it later.
Streamlit provides the browser interface. The Google Gen AI SDK will connect your Python code to the Gemini API.
- Select the New File control in the Explorer view.
- Enter requirements.txt as the file name.
- Add the pinned dependencies by pasting this code into requirements.txt:
streamlit==1.66.1
google-genai==2.33.0
What does this file do?
- The first line pins the package that renders your Python app in the browser.
- The second line pins the package that sends requests to Gemini.
- The exact versions keep your installation aligned with the code in this project.
- Save requirements.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
The Explorer view should now list requirements.txt inside ask-my-pdf.
The first package installation can take a minute while Python downloads both dependencies. Terminal activity during the wait means the installation is progressing.
- Install the pinned packages into the active virtual environment by running:
python -m pip install -r requirements.txt
What does this command do?
Python runs pip from the active virtual environment. Pip reads requirements.txt before installing both pinned packages into .venv.
The terminal should return to its prompt without an installation error. Both dependencies are now available to the selected interpreter.
Package installation failed?
Check that the terminal prompt starts with (.venv). Activate the environment before repeating the installation if that prefix is missing.
Compare requirements.txt with the reference below. A changed package name or version can block installation.
If the failure continues, help me diagnose my requirements installation error.
✔️ Awesome, I've got everything!
Great. Double check that you saved requirements.txt in Visual Studio Code.
ⓧ I'd like to double check the full code
Compare your complete requirements.txt file with this reference.
streamlit==1.66.1
google-genai==2.33.0
What should match?
Your file should contain these two lines in this order. Matching the package names plus version pins gives you the target environment.
An API key authorizes the SDK to make Gemini requests from your local app. Storing it in the shell keeps the live credential out of your project files.
Creating a credential can feel sensitive. You will paste it only into the current terminal without capturing it in a screenshot.
Keep Your PDF Public
Gemini 3.8 Flash input plus output are free of charge within the free tier. Free-tier content is used to improve Google's products.
Use the public or sample PDF you selected earlier. Keep private documents outside this project.
- Open the Gemini API key setup guide in your browser.
- Follow the guide into Google AI Studio.
- Sign in with your Google account if prompted.
- Create a Gemini API key through the on-screen flow.
- Copy the new API key.
The copied value is a live credential. Keep it out of project files plus screenshots.
- Return to the fresh Visual Studio Code terminal from earlier.
- Replace your-api-key-here with your copied key in the command below.
- Set the key for the current shell by running:
export GEMINI_API_KEY="your-api-key-here"
What does this command do?
The command stores your credential under GEMINI_API_KEY for the current shell. The Google Gen AI SDK automatically recognizes this environment variable.
The value remains available while this terminal stays open. Your source files remain free of the credential.
The final check starts Streamlit's included example app. The server keeps this terminal busy while the browser page is running.
Before you run the command, what do you expect your browser to show?
- Launch the Streamlit Hello app by running this command:
python -m streamlit hello
What does this command do?
Python runs the installed Streamlit package from .venv. Streamlit starts its Hello app before opening it in your default browser.
The server continues running until you press Ctrl+C in the terminal. Keep it running while you complete the checkpoint.
You should see Streamlit's Hello app in a new browser tab. That page proves your interpreter plus virtual environment plus package installation work together.
Hello app did not open?
Check the terminal for the local address printed by Streamlit. Paste that address into your browser if the tab did not open automatically.
If the terminal mentions a missing Streamlit package, confirm that .venv is active. Repeat the requirements installation inside that environment.
If the app still does not launch, help me troubleshoot the Streamlit Hello app.
Your Python workspace now works from editor to browser. Next, you will replace the example page with the visible shell for Ask My PDF.
Build the Visible Chat Shell
Your Python workspace is ready. Now you can turn that verified setup into the first visible version of your app.
In this step, you will build the browser interface with Streamlit. Its temporary echo will prove that the interface works while exposing why a user interface alone cannot answer questions about a PDF.
In this step, get ready to:
- Create a PDF uploader that accepts files up to 50 MB.
- Keep the chat input disabled until a PDF is present.
- Test user and assistant chat bubbles with a temporary echo.
Create the upload interface
The uploader gives your app one document to work with. Limiting it to PDF files keeps the interface aligned with the questions this chatbot will answer.
- In the Visual Studio Code file sidebar, click the new-file icon.
- Type app.py into the filename field.
- Press Enter to create the file.
You should see app.py in the file sidebar. The new file opens in the editor.
- Build the title and uploader by pasting this code into app.py:
import streamlit as st
st.title("Ask My PDF")
uploaded_file = st.file_uploader(
"Upload a public PDF",
type="pdf",
max_upload_size=50,
)
What Does This Code Do?
- The import streamlit as st line makes Streamlit available through the shorter st name.
- The st.title() call places the app name at the top of the page.
- The uploaded_file variable holds the file selected in the browser.
- The type="pdf" setting limits the uploader to PDF files.
- The max_upload_size=50 setting caps the uploaded PDF at 50 MB.
- Save app.py in Visual Studio Code.
- Return to the terminal from the previous step.
- Stop the running Hello app by pressing Ctrl+C.
You should see the shell prompt return. That terminal is ready to start your own app.
- Launch your new Streamlit app by running this command:
python -m streamlit run app.py
What Does This Command Do?
- The python -m streamlit portion starts Streamlit with the Python interpreter from your active virtual environment.
- The run app.py portion selects your new app file.
- The server stays active in this terminal while you use the browser app.
Your browser should show Ask My PDF above an uploader. Good progress. Your own app.py interface is now running.
App Did Not Open?
If the browser stays closed, use the browser address printed in the terminal. Keep the Streamlit process running while you open that address.
If the terminal reports that Streamlit is unavailable, switch back to the fresh terminal where your existing .venv is active.
Help me diagnose why my Streamlit app does not open in the browser.
Lock and unlock the chat input
A document chatbot needs a document before it can accept a useful question. The input starts disabled so the interface guides the user into uploading a PDF first.
- Add the locked chat input by pasting this code directly below the uploaded_file block in app.py:
if uploaded_file is None:
st.info("Upload a PDF to unlock the chat.")
prompt = st.chat_input(
"Ask a question about the PDF",
disabled=uploaded_file is None,
submit_mode="disable",
)
How Does the Input React to the File?
- The if uploaded_file is None check detects when the user has not selected a PDF.
- The st.info() call explains what the user needs to do next.
- The st.chat_input() call creates the question field.
- The disabled=uploaded_file is None setting keeps that field locked while the uploader is empty.
- The prompt variable holds the submitted question.
- Save app.py.
- Refresh the browser tab containing your app.
You should see the upload reminder above a disabled chat field. The field cannot accept a question yet.
- Use the uploader to select the public PDF you chose earlier.
You should see the PDF filename in the uploader. The reminder disappears. The chat input becomes available.
Chat Input Still Disabled?
Confirm that the selected file is a PDF no larger than 50 MB. Remove any unsupported file before selecting your public PDF again.
If the filename is visible but the input remains disabled, compare the uploaded_file spelling in the uploader and chat input blocks.
Help me troubleshoot why my Streamlit chat input stays disabled after I upload a PDF.
Add the temporary echo and test the shell
The chat shell needs to display both sides of a conversation before AI is added. A temporary echo creates that visible loop with predictable text.
- Add the user and assistant bubbles by pasting this code at the end of app.py:
if prompt:
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
st.markdown(f"Temporary echo: {prompt}")
How Does the Echo Work?
- The if prompt block runs only after the user submits a question.
- The first st.chat_message() block displays the submitted question as a user message.
- The second st.chat_message() block displays an assistant message.
- The temporary f-string repeats the submitted prompt so you can verify that text moves through the interface.
- Save app.py.
- Refresh the browser tab containing your app.
- Select your public PDF in the uploader.
- Type What is this document about? into the unlocked chat input.
Before you submit the question, do you think this interface can discover what the PDF contains?
- Submit the question by pressing Enter.
You should see the uploaded filename plus a user message containing your question. The assistant message reads Temporary echo: What is this document about?.
The assistant bubble repeats your words without inspecting the PDF. That intended shortfall proves the chat shell works while revealing the document grounding gap.
Message Bubbles Missing?
Confirm that the PDF filename is visible before submitting the question. The chat input must be unlocked for prompt to receive text.
Check that both st.chat_message() blocks are indented inside if prompt:.
Help me fix missing user or assistant chat bubbles in my Streamlit echo interface.
✔️ Awesome, I've got everything!
Your app.py file now contains the complete visible chat shell. Double check that you saved it in Visual Studio Code.
ⓧ I'd like to double check the full code
Compare your app.py file with this complete version.
import streamlit as st
st.title("Ask My PDF")
uploaded_file = st.file_uploader(
"Upload a public PDF",
type="pdf",
max_upload_size=50,
)
if uploaded_file is None:
st.info("Upload a PDF to unlock the chat.")
prompt = st.chat_input(
"Ask a question about the PDF",
disabled=uploaded_file is None,
submit_mode="disable",
)
if prompt:
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
st.markdown(f"Temporary echo: {prompt}")
Your visible chat shell now collects a PDF and displays both sides of a conversation. Next, you will connect the chat box to Gemini. Then you will test what the model can know without receiving the PDF.
Meet the Ungrounded Chatbot
Your Streamlit chat shell now accepts a PDF. Its temporary echo proves the browser interaction works.
The next layer is a real Gemini response through the Google Gen AI SDK. This test asks whether selecting a file in the browser gives the model enough context to inspect the document.
In this step, get ready to:
- Read the Gemini API key from the current shell.
- Replace the temporary echo with a text-only Gemini request.
- Test whether Gemini can inspect the selected PDF.
Load the Gemini API key
An API key identifies your request to Gemini. Reading it from the environment keeps the credential outside app.py.
- In app.py, replace the existing import with this import group:
import os
import streamlit as st
from google import genai
What Do These Imports Do?
- The os module reads values provided by the current shell.
- The st alias keeps the existing Streamlit interface code concise.
- The genai import provides the client that sends requests to Gemini.
- Save app.py.
- Confirm the browser reloads to the Ask My PDF interface.
Seeing an Import Error?
Return to the terminal with the activated .venv. The Google Gen AI SDK must be available to the Python interpreter running Streamlit.
Ask for help if the import still fails: help me diagnose why app.py cannot import genai from google in my activated virtual environment
The app should stop early when GEMINI_API_KEY is unavailable. A visible message makes the configuration problem clear before any request reaches Gemini.
- In app.py, find st.title("Ask My PDF").
- Add this configuration check directly below that line:
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
st.error("Set the GEMINI_API_KEY environment variable before starting the app.")
st.stop()
How Does the Configuration Gate Work?
- The os.getenv call reads GEMINI_API_KEY from the current shell.
- The if not api_key condition detects a missing value.
- The st.error call displays a readable configuration message.
- The st.stop call prevents the rest of the app from running without a key.
- Save app.py.
- Confirm the uploader remains visible without a configuration warning.
That configuration gate is working. Your app now reaches the interface only when the current shell provides the API key.
Seeing the API Key Warning?
Return to the shell from the setup step. If you opened a different terminal, set the key there using the same export command you used earlier.
Restart Streamlit from that shell after the variable is available. The new process can then read GEMINI_API_KEY.
Ask for help without sharing the key: help me check why my Streamlit process cannot read GEMINI_API_KEY
Replace the echo with Gemini
A model can use only the content included in its request. This first request contains the user's question plus an explicit statement about the missing document context.
- In app.py, find the assistant block containing st.markdown(f"Temporary echo: {prompt}").
- Replace that assistant block with the following request code:
ungrounded_prompt = f"""
Answer this question: {prompt}
No document has been provided, so explain that you cannot inspect the uploaded PDF.
"""
with st.chat_message("assistant"):
with genai.Client(api_key=api_key) as client:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=ungrounded_prompt,
)
answer = response.text or "I cannot inspect the PDF."
st.markdown(answer)
What Does This Code Do?
- The ungrounded_prompt variable combines the user's question with the current request boundary.
- The client context manager opens an authenticated Gemini connection for this request.
- The generate_content call sends only ungrounded_prompt to gemini-3.8-flash.
- The response.text value becomes the assistant message.
- The fallback sentence keeps the assistant bubble readable when the response contains no text.
- Save app.py.
- Confirm the browser reloads to the Ask My PDF interface.
Does the App Stop After Saving?
Check that ungrounded_prompt and the assistant block remain indented inside if prompt:.
Compare the triple quotes around the prompt carefully. A missing quote prevents Python from reading the rest of the file.
Ask for targeted help: help me find the indentation or string error in my ungrounded Gemini request
✔️ Awesome, I've got everything!
Great. Double check that you saved app.py before running the document-access test.
ⓧ I'd like to double check the full code
Compare your complete app.py file with this reference.
import os
import streamlit as st
from google import genai
st.title("Ask My PDF")
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
st.error("Set the GEMINI_API_KEY environment variable before starting the app.")
st.stop()
uploaded_file = st.file_uploader(
"Upload a public PDF",
type="pdf",
max_upload_size=50,
)
if uploaded_file is None:
st.info("Upload a PDF to unlock the chat.")
prompt = st.chat_input(
"Ask a question about the PDF",
disabled=uploaded_file is None,
submit_mode="disable",
)
if prompt:
with st.chat_message("user"):
st.markdown(prompt)
ungrounded_prompt = f"""
Answer this question: {prompt}
No document has been provided, so explain that you cannot inspect the uploaded PDF.
"""
with st.chat_message("assistant"):
with genai.Client(api_key=api_key) as client:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=ungrounded_prompt,
)
answer = response.text or "I cannot inspect the PDF."
st.markdown(answer)
How Does the Full File Work?
This file validates the API key before rendering the uploader. It replaces the echo with a Gemini request that contains text but no PDF data.
Run the document-access test
The interface now sends a real request to Gemini. The planned test checks what the model can infer from the request it receives.
- Return to the terminal running Streamlit from earlier.
- Stop the current server by pressing Ctrl+C.
Before you relaunch, what do you expect the assistant to say when it receives only the text request?
- Relaunch the updated app by running this command:
python -m streamlit run app.py
What Does This Command Do?
This command uses the active Python environment to run app.py through Streamlit. The browser receives the updated interface when the server starts.
- Return to the browser tab running Ask My PDF.
- Select your public PDF through the Upload a public PDF uploader.
- Confirm the uploaded filename is visible above the chat field.
- Enter What is the exact title of the PDF I uploaded? in the chat input.
- Submit the question by pressing Enter.
You will see your question in a user bubble. The assistant response explains that it cannot inspect the uploaded PDF.
You have exposed the exact grounding gap. The model can answer a text request without gaining access to the file selected in Streamlit.
Did the Request Fail Instead?
Confirm that the terminal still uses the activated .venv. Also confirm the current shell still provides the API key.
A temporary API limit or network problem can also block the request. Wait briefly before submitting the same question again.
Ask for help without including your credential: help me diagnose why my text-only Gemini request fails in Streamlit
Your chatbot now reaches Gemini while exposing the missing document context. Next, you will close that grounding gap so the assistant can answer from the PDF.
Ground Every Answer in the PDF
Your Streamlit interface now reaches Gemini. The model still receives only the typed question.
The selected PDF stays inside Streamlit because the request never sends its bytes. That missing evidence creates the PDF grounding gap you saw in the previous step.
This step attaches the PDF as an application/pdf part. Gemini can then inspect the document before answering.
In this step, get ready to:
- Send the PDF bytes with every submitted question.
- Give Gemini a strict document-only instruction.
- Repeat the title question to prove the answer uses the PDF.
Import the PDF request type
The Google Gen AI SDK uses types.Part to package binary data with a media type. That wrapper tells Gemini how to read the bytes.
- Return to app.py from the previous step.
- Find from google import genai near the top of the file.
- Add the PDF request type directly below that import by inserting this line:
from google.genai import types
What does this import add?
- The types module contains the SDK structures used to describe request content.
- The Part structure gives the PDF bytes a media type that Gemini can interpret.
Send the PDF with each question
A grounded request needs the uploaded document plus a clear instruction. Their position in the same request gives Gemini the evidence it needs for the answer.
- Find the if prompt: block near the bottom of app.py.
- Select the complete handler through the final st.markdown(answer) line.
- Replace the selected handler by pasting this code:
if prompt:
with st.chat_message("user"):
st.markdown(prompt)
grounded_prompt = f"""
Answer the question using only the attached PDF.
If the PDF does not contain the answer, reply exactly: I can't find that in the PDF.
Question:
{prompt}
"""
with st.chat_message("assistant"):
with genai.Client(api_key=api_key) as client:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=[
types.Part.from_bytes(
data=uploaded_file.getvalue(),
mime_type="application/pdf",
),
grounded_prompt,
],
)
answer = response.text or "I can't find that in the PDF."
st.markdown(answer)
How does this ground the answer?
- The grounded_prompt limits the evidence source to the attached PDF.
- The uploaded_file.getvalue() call reads the selected document as bytes.
- The types.Part.from_bytes call labels those bytes as application/pdf.
- The contents list sends the PDF part before the grounded question.
- The fallback answer keeps missing information from becoming an unsupported claim.
- Save app.py.
- Return to the terminal from the previous test.
- Stop the current Streamlit process by pressing Ctrl+C.
- Launch the updated app by running this command:
python -m streamlit run app.py
What does this command do?
This starts app.py through the Streamlit package installed in your active virtual environment. The local app opens in your browser.
Good progress. Your updated Ask My PDF page loads without a Python error.
Does the app stop with an error?
- Compare the indentation inside your if prompt: block with the code above.
- Confirm from google.genai import types sits directly below the existing Google import.
- Check that every opening parenthesis has a matching closing parenthesis.
Ask for help with the grounded request error.
Prove Gemini can inspect the PDF
The title question exposed the missing document in the previous step. A document-specific response now proves that the request includes the PDF bytes.
Before you submit the same question, do you expect Gemini to identify the title from the PDF this time?
- Return to the browser tab opened by Streamlit.
- Select the same public PDF through Upload a public PDF.
- Enter What is the exact title of the PDF I uploaded? in the chat input.
- Press Enter to submit the question.
The assistant now gives a document-specific title from the uploaded PDF. That is the grounding gap closed.
Still getting an ungrounded answer?
- Confirm the contents value is a list containing the PDF part.
- Confirm grounded_prompt appears after the PDF part inside that list.
- Confirm the selected file is a public PDF no larger than 50 MB.
Help me diagnose why Gemini still cannot inspect my uploaded PDF.
✔️ Awesome, I've got everything!
Your app.py now sends the uploaded PDF with every question.
ⓧ I'd like to double check the full code
The complete app.py file is below for reference.
import os
import streamlit as st
from google import genai
from google.genai import types
st.title("Ask My PDF")
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
st.error("Set the GEMINI_API_KEY environment variable before starting the app.")
st.stop()
uploaded_file = st.file_uploader(
"Upload a public PDF",
type="pdf",
max_upload_size=50,
)
if uploaded_file is None:
st.info("Upload a PDF to unlock the chat.")
prompt = st.chat_input(
"Ask a question about the PDF",
disabled=uploaded_file is None,
submit_mode="disable",
)
if prompt:
with st.chat_message("user"):
st.markdown(prompt)
grounded_prompt = f"""
Answer the question using only the attached PDF.
If the PDF does not contain the answer, reply exactly: I can't find that in the PDF.
Question:
{prompt}
"""
with st.chat_message("assistant"):
with genai.Client(api_key=api_key) as client:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=[
types.Part.from_bytes(
data=uploaded_file.getvalue(),
mime_type="application/pdf",
),
grounded_prompt,
],
)
answer = response.text or "I can't find that in the PDF."
st.markdown(answer)
What should match?
This reference shows the cumulative app at the end of this step. The imports plus the complete if prompt: handler should match your file.
Your chatbot can now read the selected document before it answers. Next, you will preserve the conversation across reruns plus turn request failures into readable chat messages.
Preserve the Conversation and Handle Failures
Your app can now send the uploaded PDF to Gemini. That hard part is done: Gemini can return a document-specific answer.
A single answer still disappears from the conversation unless the app saves it across Streamlit reruns. An API failure can also replace the chat with a broken page.
In this step, Session State keeps the conversation visible. Exception handling turns request failures into readable assistant messages.
In this step, get ready to:
- Persist chat messages across reruns.
- Add visible conversation context to follow-up requests.
- Turn request failures into readable assistant messages.
Preserve visible chat history
Streamlit reruns app.py from top to bottom after each interaction. Session State stores values for the current browser session.
Each saved message needs a role plus its content. The replay loop uses those values to rebuild the message bubbles.
- Find the block from if uploaded_file is None: through st.markdown(prompt) in app.py.
- Replace the selected block with the code below:
if "messages" not in st.session_state:
st.session_state.messages = []
if uploaded_file is None:
st.info("Upload a PDF to unlock the 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 about the PDF",
disabled=uploaded_file is None,
submit_mode="disable",
)
if prompt:
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
How Does Message Replay Work?
- The initialization creates st.session_state.messages once for the current browser session.
- The replay loop renders every saved dictionary with its original chat role.
- The submitted prompt is saved before its user bubble is rendered.
- Find st.markdown(answer) near the bottom of app.py.
- Add the code below immediately after that line:
st.session_state.messages.append(
{"role": "assistant", "content": answer}
)
Why Save the Answer Here?
The assistant answer is saved after Streamlit displays it. The next rerun can replay the completed user-assistant pair.
- Save app.py.
- Run the updated app from the activated terminal with this command:
python -m streamlit run app.py
What Does This Command Do?
This command launches app.py with Streamlit from your active virtual environment. The app opens in a browser tab.
- Keep the terminal running while you test the app.
- Upload your public PDF if the uploader is empty.
- Ask What is this document mainly about? in the chat input.
- Ask Which part of the PDF supports that answer? as a follow-up question.
You will see the first user-assistant pair remain visible when the second pair appears. The chat now survives Streamlit reruns.
Do Earlier Messages Disappear?
Check that the replay loop sits above st.chat_input. Code below the input cannot rebuild the history before the next message is submitted.
Confirm that the user append sits inside if prompt:. Confirm that the assistant append sits after st.markdown(answer).
Help me fix disappearing Streamlit chat history.
Build follow-up context
Visible history helps the user follow the conversation. Gemini also needs that history inside the next request to understand phrases such as “that section” or “the previous answer.”
The PDF remains the evidence source for every answer. Earlier messages provide context without becoming evidence.
- Find the grounded_prompt block in app.py.
- Replace that block with the code below:
conversation = "\n".join(
f"{message['role']}: {message['content']}"
for message in st.session_state.messages
)
grounded_prompt = f"""
Answer the latest question using only the attached PDF.
Use the conversation for follow-up context, but do not treat earlier answers as evidence.
If the PDF does not contain the answer, reply exactly: I can't find that in the PDF.
Conversation:
{conversation}
"""
How Does Follow-Up Context Work?
- The conversation value joins every saved message into one text transcript.
- The updated instructions tell Gemini to interpret the latest question using that transcript.
- The attached PDF remains the only allowed source for factual answers.
- Save app.py.
- Return to the browser tab running Ask My PDF.
- Ask What is the main recommendation in this PDF? in the chat input.
- Ask What evidence does it give for that? as the next question.
You will see the earlier messages remain above the new answer. Gemini can use the first question to interpret what “that” means.
Does the Follow-Up Ignore the First Answer?
Check that conversation is created before grounded_prompt. The prompt must contain {conversation} under the conversation label.
Help me troubleshoot missing follow-up context.
Make requests resilient
A Gemini request can take time because the model reads the PDF before answering. A spinner confirms that the app is still working.
Requests can also fail because of network or service conditions. A try block keeps successful responses on the normal path.
- Find the assistant block from with st.chat_message("assistant"): through st.markdown(answer) in app.py.
- Replace the selected block with the code below:
with st.chat_message("assistant"):
with st.spinner("Reading the PDF..."):
try:
with genai.Client(api_key=api_key) as client:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=[
types.Part.from_bytes(
data=uploaded_file.getvalue(),
mime_type="application/pdf",
),
grounded_prompt,
],
)
answer = response.text or "I can't find that in the PDF."
except Exception as error:
answer = f"Request failed: {error}"
st.markdown(answer)
How Does This Protect the Chat?
- The spinner displays Reading the PDF... while the request is running.
- The try block handles the normal Gemini response.
- The except block converts an exception into an answer beginning with Request failed:.
- The final st.markdown(answer) displays either result inside the assistant bubble.
Before you test the finished app, do you expect the absent question to produce a guess or the exact fallback sentence?
- Save app.py.
- Return to the browser tab running Ask My PDF.
- Ask What is this document mainly about? in the chat input.
- Ask Which part of the PDF supports that answer? as the related follow-up.
- Ask What is the author's favorite breakfast? as the absent-answer test.
During each request, you will see Reading the PDF.... After all three questions, you will see six new message bubbles remain visible.
The final assistant bubble reads I can't find that in the PDF. If a request fails, its assistant bubble begins with Request failed:.
That is the full chatbot working: follow-up context stays visible across reruns. Failed requests now stay inside a readable assistant bubble.
Is the Final Check Falling Short?
If messages disappear, compare the initialization plus both append locations with the full file below. Every saved dictionary needs a role plus content.
If the absent question produces a guess, check the exact fallback instruction inside grounded_prompt.
Help me debug the finished chat flow.
✔️ Awesome, I've got everything!
Great. Double check that you saved app.py after the final test.
ⓧ I'd like to double check the full code
Compare your complete app.py file with this version.
import os
import streamlit as st
from google import genai
from google.genai import types
st.title("Ask My PDF")
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
st.error("Set the GEMINI_API_KEY environment variable before starting the app.")
st.stop()
uploaded_file = st.file_uploader(
"Upload a public PDF",
type="pdf",
max_upload_size=50,
)
if "messages" not in st.session_state:
st.session_state.messages = []
if uploaded_file is None:
st.info("Upload a PDF to unlock the 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 about the PDF",
disabled=uploaded_file is None,
submit_mode="disable",
)
if prompt:
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
conversation = "\n".join(
f"{message['role']}: {message['content']}"
for message in st.session_state.messages
)
grounded_prompt = f"""
Answer the latest question using only the attached PDF.
Use the conversation for follow-up context, but do not treat earlier answers as evidence.
If the PDF does not contain the answer, reply exactly: I can't find that in the PDF.
Conversation:
{conversation}
"""
with st.chat_message("assistant"):
with st.spinner("Reading the PDF..."):
try:
with genai.Client(api_key=api_key) as client:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=[
types.Part.from_bytes(
data=uploaded_file.getvalue(),
mime_type="application/pdf",
),
grounded_prompt,
],
)
answer = response.text or "I can't find that in the PDF."
except Exception as error:
answer = f"Request failed: {error}"
st.markdown(answer)
st.session_state.messages.append(
{"role": "assistant", "content": answer}
)
Secret mission
Add a Safe Reset Control
Chat history from one PDF can confuse the next conversation. Add a reset control that clears every saved message before the user selects another public PDF.
Clean Up Your Resources
Clean Up Your Resources
Your chatbot runs locally with no paid resource left running. Choose whether to keep the workspace, pause the app, or delete the local files.
Resources you used:
- The local Streamlit process that serves the chatbot.
- The .venv folder containing Streamlit plus the Google Gen AI SDK.
- The app.py application file.
- The requirements.txt dependency file.
- The GEMINI_API_KEY value exported in the current terminal session.
Keep everything running
Keep the workspace as it is when you want to continue testing the chatbot or show it to someone.
- Leave the Streamlit process running while you continue using the browser app.
- Keep app.py, requirements.txt, and .venv in the currently open project folder.
- Keep the terminal session open while the app needs its exported GEMINI_API_KEY value.
Pause - I'll come back to this later
Pause the app when you want to free local memory without losing your work. Your files stay ready for the next session.
- Switch back to the terminal window from earlier.
- Press Control+C to stop the running Streamlit process.
- Close the terminal window to end the activated virtual environment plus its temporary API key value.
- Leave app.py, requirements.txt, and .venv in the currently open project folder.
Delete - I don't want to use this again
Delete the local workspace when you do not plan to revisit this app. This cleanup removes the resources stored on your Mac.
- Switch back to the terminal window from earlier.
- Press Control+C if the Streamlit process is still running.
- Clear the exported API key value from the current shell by running this command:
unset GEMINI_API_KEY
Why Clear the Shell Value?
This command clears the exported API key value from the current terminal session. It prevents later commands in this shell from using that credential.
Good call. That credential is now cleared from this terminal session.
- Return to the Visual Studio Code window from earlier.
- Confirm its file list contains app.py plus requirements.txt.
Deleting these files is permanent. The next command targets only .venv, app.py, and requirements.txt in the current folder.
- Remove the virtual environment plus both project files by running this command:
rm -rf .venv app.py requirements.txt
What Does This Cleanup Command Do?
The command removes the virtual environment plus the two named project files from the current folder. Unrelated files remain untouched.
- Check the file list in the same Visual Studio Code window.
You should no longer see .venv, app.py, or requirements.txt.
Files Still Visible?
- Confirm the terminal is attached to the same folder shown in the Visual Studio Code window.
Help me troubleshoot the local cleanup.
- Close the current terminal window.
Cleanup is complete. Your local application files plus the active shell session are gone.
Nice Work!
Nice Work!
You did it! Ask My PDF now answers questions from one public PDF in a browser.
You learned how to:
- Built a browser chat interface with Streamlit that unlocks after a PDF upload.
- Connected Gemini 3.8 Flash through the Google Gen AI SDK. Sent PDF bytes as multimodal context for document-grounded answers.
- Preserved follow-up context with Streamlit Session State. Added an exact fallback for missing information. Turned API failures into readable chat messages.
- Secret Mission: Added a safe reset control that clears old messages before the user selects another PDF.
Ready to quiz yourself?