Build an AI Document Q&A Tool
Chat with any PDF using Claude's API and prompt engineering patterns.
Introduction
30 Second Summary
You have a 50-page report and need to find one specific answer buried somewhere in it. Instead of skimming every page, imagine asking a question and getting the exact answer back instantly.
In this project, you will build a Python command-line tool that extracts text from any PDF and lets you have a conversation about it using Claude's API.
What You'll Build
A CLI tool called docqa that takes a PDF file, extracts its text, and uses Claude to answer questions about the document's contents.
By the end of this project, you'll have:
- A PDF text extraction module that reads and processes any PDF document.
- A Claude API integration with system prompts and context injection for document-grounded answers.
- A multi-turn conversation loop that maintains history so Claude remembers your previous questions.
- Secret Mission: Add a "summarize" command that generates a structured summary with key points, action items, and important dates.
Are there any prerequisites?
You need basic Python knowledge (functions, file I/O, and string manipulation) and an Anthropic API key. If you don't have an API key yet, you can sign up at Anthropic's console before starting Step 1.
Set Up Your Environment
Before you can chat with a PDF, you need the right tools installed. The Anthropic Python SDK handles communication with Claude, and pypdf handles reading PDF files.
In this step, you will install both packages, configure your API key, and confirm everything connects successfully.
In this step, get ready to:
- Install the anthropic SDK and pypdf library.
- Configure your Anthropic API key as an environment variable.
- Test the API connection with a simple message to Claude.
Install the required Python packages
Your project needs two packages. The anthropic package lets your Python code talk to Claude's API. The pypdf package reads and extracts text from PDF files.
- Navigate into your project folder by running this command:
cd ~/Desktop/docqa
- Install both packages by running:
pip install anthropic pypdf
What do these packages do?
- anthropic is the official Python SDK for the Claude API. It handles authentication, request formatting, and response parsing.
- pypdf is a pure-Python library for reading PDF files. It extracts text from each page without needing any external system dependencies.
You should see output showing both packages being downloaded and installed successfully.
Seeing a permission error?
If you see "Permission denied", try running the command with pip install --user anthropic pypdf instead.
If pip is not found, your Python installation may use pip3 instead. Try pip3 install anthropic pypdf.
help me fix a pip installation error
Configure your API key
The Anthropic SDK looks for an environment variable called ANTHROPIC_API_KEY when it creates a client. Setting this variable lets your script authenticate without hardcoding secrets in your code.
macOS
- Set your API key for this terminal session by running:
export ANTHROPIC_API_KEY='your-api-key-here'
- Replace your-api-key-here with your actual Anthropic API key.
Windows
- Set your API key for this PowerShell session by running:
$env:ANTHROPIC_API_KEY="your-api-key-here"
- Replace your-api-key-here with your actual Anthropic API key.
Why an environment variable?
Environment variables keep sensitive values out of your source code. If you ever share or commit your script, the key stays private because it lives in your terminal session, not in the file.
Key only lasts for this session
This command sets the key for your current terminal window only. If you close the terminal and reopen it, you will need to run the export command again before running your script.
Create the script file and test the connection
Now you will create the main script file and send a quick test message to Claude. This confirms your packages, API key, and network connection all work together.
- Create a new file called docqa.py inside your docqa folder.
- Open docqa.py in your code editor and paste the following code:
import sys
import os
from pypdf import PdfReader
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=32,
messages=[{"role": "user", "content": "Say hello!"}]
)
print(response.content[0].text)
What does this code do?
- The first four lines import the libraries you will use throughout this project.
- Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable.
- client.messages.create() sends a message to the claude-sonnet-4-5 model and returns Claude's response.
- response.content[0].text extracts the text of Claude's reply from the response object.
- Save docqa.py.
- Run the script by executing this command in your terminal:
python docqa.py
You should see a short greeting from Claude printed in your terminal, something like "Hello" or "Hi there."
Seeing an authentication error?
If you see an error about an invalid API key, double-check that you ran the export (macOS) or $env: (Windows) command in the same terminal window where you are running the script.
If you see "ModuleNotFoundError", the packages did not install into the same Python version you are running. Try python3 docqa.py instead, or reinstall with pip3 install anthropic pypdf.
help me debug my Claude API connection error
✔️ Awesome, I've got everything!
Great. Double check you have saved docqa.py and seen a greeting from Claude in your terminal.
ⓧ I'd like to double check the full code
Here is the complete docqa.py file at this point:
import sys
import os
from pypdf import PdfReader
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=32,
messages=[{"role": "user", "content": "Say hello!"}]
)
print(response.content[0].text)
Your environment is ready and Claude is responding. Next up, you will teach your script to read a PDF file and extract its text.
Extract Text from a PDF
Your API connection to Claude is confirmed and working. Now it's time to build the core feature that makes this tool useful: reading PDFs.
Before you can ask Claude questions about a document, you need to get the text out of the PDF and into a format Claude can read. In this step, you'll write a function that opens any PDF, extracts the text from every page, and handles problems gracefully.
In this step, get ready to:
- Write a function that extracts text from every page of a PDF.
- Add error handling for missing files, corrupt PDFs, and empty documents.
- Test the extraction with a real PDF file.
Write the PDF extraction function
The test code in docqa.py confirmed your API key works. You'll replace it now with the real extraction function.
- In docqa.py, delete everything below the import statements. Remove these lines:
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=32,
messages=[{"role": "user", "content": "Say hello!"}]
)
print(response.content[0].text)
- Add the following function below your import statements. Leave one blank line between the imports and the function:
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
# Check if the file exists before trying to open it
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' not found.")
sys.exit(1)
# Try to read the PDF, catch any corruption or format errors
try:
reader = PdfReader(pdf_path)
except Exception as e:
print(f"Error reading PDF: {e}")
sys.exit(1)
# Check that the PDF actually has pages
if len(reader.pages) == 0:
print("Error: PDF has no pages.")
sys.exit(1)
# Loop through every page and collect text
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
# Warn if no text was extractable (common with scanned documents)
if not text.strip():
print("Warning: No text could be extracted from this PDF.")
print("The PDF might be scanned or image-based.")
sys.exit(1)
return text
What does this function do?
This function handles the full lifecycle of extracting text from a PDF, including everything that can go wrong:
- os.path.exists(pdf_path) checks that the file actually exists before attempting to open it.
- The try/except block catches corrupt or malformed PDF files that PdfReader can't parse.
- len(reader.pages) == 0 guards against an empty PDF with no pages at all.
- The loop calls page.extract_text() on every page. This method returns an empty string if no text is extractable from that page.
- The final text.strip() check catches scanned or image-based PDFs where every page returned empty text.
- Save docqa.py.
You won't see output yet. The next substep will test this function with a real PDF.
Seeing a syntax error when you save?
- Make sure you deleted all of the old test code. Leftover lines like client = Anthropic() outside a function will still run and aren't needed yet.
- Check that the function's indentation is consistent. Every line inside the function should be indented with 4 spaces.
- Verify the f-string quotes match. The line print(f"Error: File '{pdf_path}' not found.") uses double quotes on the outside and single quotes around {pdf_path}.
Still stuck? Help me fix a syntax error in my extract_text_from_pdf function in docqa.py
Test the extraction with a sample PDF
You need a PDF file to test with. Any PDF on your computer will work. Reports, receipts, lecture notes, or ebooks are all good choices.
- Find any PDF file on your computer and copy it into your docqa project folder. Note the filename (for example, sample.pdf).
- At the bottom of docqa.py, temporarily add these two lines to test your function:
# Temporary test - remove after verifying
text = extract_text_from_pdf("sample.pdf")
print(f"Extracted {len(text)} characters.")
print(text[:500])
What does this test code do?
It calls your function with the PDF filename, then prints the total character count and the first 500 characters. This confirms the extraction works without flooding your terminal with an entire document.
- Replace sample.pdf in the test code with your actual PDF filename if it's different.
- Save docqa.py and run the script by running:
python3 docqa.py
You should see a character count followed by the first 500 characters of your PDF's text. The text might include some formatting quirks like extra spaces or missing line breaks. That's normal for PDF extraction.
- Once you've confirmed the extraction works, delete the temporary test lines from the bottom of docqa.py.
- Save docqa.py.
Not seeing any output?
- Check that your PDF file is in the same folder as docqa.py. If it's in a different location, use the full path in your test code.
- If you see the "No text could be extracted" warning, your PDF might be a scanned document (essentially images). Try a different PDF that contains selectable text.
- If you see "File not found", double-check the filename matches exactly, including the .pdf extension and capitalization.
Need more help? Help me troubleshoot my PDF text extraction in docqa.py
✔️ Awesome, I've got everything!
Great. Double check you've saved your file after removing the test lines.
ⓧ I'd like to double check the full code
import sys
import os
from pypdf import PdfReader
from anthropic import Anthropic
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' not found.")
sys.exit(1)
try:
reader = PdfReader(pdf_path)
except Exception as e:
print(f"Error reading PDF: {e}")
sys.exit(1)
if len(reader.pages) == 0:
print("Error: PDF has no pages.")
sys.exit(1)
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
if not text.strip():
print("Warning: No text could be extracted from this PDF.")
print("The PDF might be scanned or image-based.")
sys.exit(1)
return text
Your tool can now read any text-based PDF. Next up, you'll connect this extracted text to Claude so you can start asking questions about your documents.
Build the Claude Integration
Your PDF extraction function can now pull text from any document. The next challenge is sending that text to Claude so it can answer questions about it.
The core pattern here is context injection. You embed the document text into a system prompt, then send the user's question as a message. Claude reads the document in its context and answers based only on what it finds there.
In this step, get ready to:
- Write the ask_claude function with a system prompt that embeds document text.
- Configure the Claude API call with the right model and token parameters.
- Verify the integration code loads without errors.
Write the ask_claude function
This function takes the extracted document text, wraps it in a system prompt, and sends it to Claude alongside the user's question. It also tracks conversation history so Claude can reference earlier exchanges.
- In docqa.py, add the following function below your extract_text_from_pdf function:
def ask_claude(client, document_text, conversation_history, user_question):
"""Send a question to Claude with document context."""
# Build the system prompt with the full document embedded
system_prompt = f"""You are a helpful document analysis assistant. Answer questions based ONLY on the following document content. If the answer is not in the document, say so clearly.
DOCUMENT CONTENT:
{document_text}"""
# Add the user's question to conversation history
conversation_history.append({
"role": "user",
"content": user_question
})
# Send the request to Claude
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=conversation_history
)
# Extract Claude's text response
assistant_message = response.content[0].text
# Save Claude's response in conversation history
conversation_history.append({
"role": "assistant",
"content": assistant_message
})
return assistant_message
What does this code do?
The system_prompt f-string embeds the entire document into Claude's instructions. The prompt tells Claude to answer ONLY from the document, preventing hallucinated answers.
The conversation_history list stores every message (user and assistant) so Claude can reference earlier exchanges. Each call to client.messages.create() sends the full history, because the Messages API is stateless.
Understand the API configuration
The client.messages.create() call has four key parameters that control how Claude responds.
Why these parameters?
- model="claude-sonnet-4-5" selects Claude Sonnet 4.5. It offers the best balance of speed and intelligence for an interactive Q&A tool at $3/MTok input and $15/MTok output.
- max_tokens=1024 caps each response at 1024 tokens. This keeps answers focused and prevents Claude from generating overly long replies.
- system=system_prompt passes the document text as a system-level instruction. Claude treats system prompts as persistent context for the entire conversation.
- messages=conversation_history sends the full conversation so far. Claude sees all previous questions and answers, which enables follow-up questions.
What is a system prompt?
A system prompt is a special instruction that shapes how an LLM behaves for the entire conversation. Unlike regular messages, it sets the rules Claude follows without appearing in the chat itself.
Here, the system prompt does two things: it tells Claude to only answer from the document (preventing hallucinations), and it injects the entire document as context. This is the core pattern behind most AI document tools.
Verify your integration code
The function is ready, but you cannot fully test it until you build the conversation loop in the next step. For now, verify that Python can load the file without syntax errors.
- Save docqa.py.
- Confirm your code has no syntax errors by running this command:
python3 -c "import docqa; print('Functions loaded successfully')"
You should see Functions loaded successfully printed with no errors. If Python finds a syntax mistake, it will point to the exact line number.
Seeing a SyntaxError?
- Check that the triple-quoted f-string in system_prompt uses three double quotes on both the opening and closing. A missing quote is the most common cause.
- Make sure the conversation_history.append() dictionaries use matching curly braces. Each dictionary needs an opening { and closing }.
- Verify you are running the command from inside your docqa folder where docqa.py lives.
Help me fix this SyntaxError in my ask_claude function
✔️ Awesome, I've got everything!
Great. Double check you have saved docqa.py with both functions in place.
ⓧ I'd like to double check the full code
Here is the full docqa.py file as it should look at the end of this step:
import sys
import os
from pypdf import PdfReader
from anthropic import Anthropic
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' not found.")
sys.exit(1)
try:
reader = PdfReader(pdf_path)
except Exception as e:
print(f"Error reading PDF: {e}")
sys.exit(1)
if len(reader.pages) == 0:
print("Error: PDF has no pages.")
sys.exit(1)
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
if not text.strip():
print("Warning: No text could be extracted from this PDF.")
print("The PDF might be scanned or image-based.")
sys.exit(1)
return text
def ask_claude(client, document_text, conversation_history, user_question):
"""Send a question to Claude with document context."""
system_prompt = f"""You are a helpful document analysis assistant. Answer questions based ONLY on the following document content. If the answer is not in the document, say so clearly.
DOCUMENT CONTENT:
{document_text}"""
conversation_history.append({
"role": "user",
"content": user_question
})
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=conversation_history
)
assistant_message = response.content[0].text
conversation_history.append({
"role": "assistant",
"content": assistant_message
})
return assistant_message
Your Claude integration is wired up. Next, you will build a conversation loop so you can ask multiple questions about your document in a single session.
Add Multi-Turn Conversation
Right now, your tool can extract text from a PDF and send a single question to Claude. But real conversations require context.
If you ask a follow-up question like "Can you explain that in simpler terms?", Claude needs to remember what it just told you. In this step, you will build a conversation loop that maintains history across multiple exchanges and add a safety check for oversized documents.
In this step, get ready to:
- Add a token-awareness check that warns about documents too large for Claude's context window.
- Implement a conversation loop that keeps asking questions until you type "quit".
- Maintain conversation history so Claude remembers previous questions and answers.
Add a token-awareness check
Claude Sonnet 4.5 has a 200k token context window. Every time you ask a question, the entire document is sent as context. If the document is very large, it could exceed what Claude can process.
- In docqa.py, find the end of the ask_claude function and add this new function below it:
def check_document_size(document_text):
"""Warn if document might be too large for context window."""
# Estimate token count (rough heuristic: 1 token per 4 characters)
estimated_tokens = len(document_text) // 4
if estimated_tokens > 150000:
print(f"\nWarning: This document is very large (~{estimated_tokens:,} estimated tokens).")
print("Claude may not be able to process the entire document at once.")
print("Consider using a shorter document or splitting it into sections.\n")
return False
return True
What does this code do?
This function uses a rough heuristic to estimate how many tokens the document contains. Dividing character count by 4 gives an approximate token count.
If the estimate exceeds 150,000 tokens (leaving room for the system prompt and responses within Claude's 200k window), it warns the user. The function returns True if the document is safe to process and False if it is too large.
- Save docqa.py.
You will not see a change yet. The next chunk builds the main function that calls this check.
Build the conversation loop
The conversation loop is the core interaction pattern. It reads user input, sends it to Claude with the full conversation history, and prints the response. Because the Messages API is stateless, you must pass the entire history on every call.
- At the bottom of docqa.py, below the check_document_size function, add the main() function and entry point:
def main():
pdf_path = "sample.pdf" # temporary hardcoded path
document_text = extract_text_from_pdf(pdf_path)
check_document_size(document_text)
# Initialize the API client and empty conversation history
client = Anthropic()
conversation_history = []
while True:
try:
user_input = input("You: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("\nGoodbye!")
break
print("\nClaude: ", end="")
answer = ask_claude(client, document_text, conversation_history, user_input)
print(answer)
print()
if __name__ == "__main__":
main()
What does this code do?
- pdf_path = "sample.pdf" is a temporary hardcoded path. You will replace this with command-line argument parsing in the next step.
- conversation_history = [] starts as an empty list. Each time you ask a question, ask_claude appends both the user message and Claude's response to this list. On the next question, the full history gets sent to Claude so it can reference earlier answers.
- The while True loop keeps running until the user types "quit" or presses Ctrl+C.
- The except (KeyboardInterrupt, EOFError) block catches Ctrl+C so the program exits gracefully instead of showing a traceback.
Why does multi-turn conversation need history?
The Anthropic Messages API is stateless. Claude does not remember anything between API calls. To simulate a conversation, you must send every previous message (both user and assistant turns) with each new request. The conversation_history list accumulates these turns so Claude has full context of what was discussed.
- Save docqa.py.
- Make sure you have a PDF file named sample.pdf in your docqa folder (the same one you used in the previous step).
- Run the tool by entering this command in your terminal:
python docqa.py
- Type a question about your PDF and press Enter. You should see Claude respond with an answer based on the document.
- Ask a follow-up question that references the previous answer (e.g., "Can you explain that further?"). Claude should respond with context from both the document and your earlier exchange.
- Type quit to exit the loop.
Not getting a response?
- Make sure your ANTHROPIC_API_KEY environment variable is still set in the terminal session you are using. If you opened a new terminal, you need to export it again.
- Confirm that sample.pdf exists in the same folder where you run the command. The hardcoded path expects it in the current directory.
- If you see a connection error, check your internet connection. The API call requires network access.
Still stuck? Help me debug why my docqa.py conversation loop is not returning a response from Claude.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before continuing.
ⓧ I'd like to double check the full code
import sys
import os
from pypdf import PdfReader
from anthropic import Anthropic
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' not found.")
sys.exit(1)
try:
reader = PdfReader(pdf_path)
except Exception as e:
print(f"Error reading PDF: {e}")
sys.exit(1)
if len(reader.pages) == 0:
print("Error: PDF has no pages.")
sys.exit(1)
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
if not text.strip():
print("Warning: No text could be extracted from this PDF.")
print("The PDF might be scanned or image-based.")
sys.exit(1)
return text
def ask_claude(client, document_text, conversation_history, user_question):
"""Send a question to Claude with document context."""
system_prompt = f"""You are a helpful document analysis assistant. Answer questions based ONLY on the following document content. If the answer is not in the document, say so clearly.
DOCUMENT CONTENT:
{document_text}"""
conversation_history.append({
"role": "user",
"content": user_question
})
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=conversation_history
)
assistant_message = response.content[0].text
conversation_history.append({
"role": "assistant",
"content": assistant_message
})
return assistant_message
def check_document_size(document_text):
"""Warn if document might be too large for context window."""
estimated_tokens = len(document_text) // 4
if estimated_tokens > 150000:
print(f"\nWarning: This document is very large (~{estimated_tokens:,} estimated tokens).")
print("Claude may not be able to process the entire document at once.")
print("Consider using a shorter document or splitting it into sections.\n")
return False
return True
def main():
pdf_path = "sample.pdf" # temporary hardcoded path
document_text = extract_text_from_pdf(pdf_path)
check_document_size(document_text)
client = Anthropic()
conversation_history = []
while True:
try:
user_input = input("You: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("\nGoodbye!")
break
print("\nClaude: ", end="")
answer = ask_claude(client, document_text, conversation_history, user_input)
print(answer)
print()
if __name__ == "__main__":
main()
Your Q&A tool now supports full multi-turn conversations. Claude remembers everything you discussed in a session. Next up, you will polish the CLI experience with proper argument parsing and a welcome banner.
Polish the CLI Experience
Your document Q&A tool works, but using it feels clunky. The PDF path is hardcoded, there is no feedback when the tool starts, and the user has no idea what commands are available.
In this step, you will turn the tool into a proper command-line application. You will add argument parsing, a welcome banner, and a summary command that gives users a quick overview of any document.
In this step, get ready to:
- Add command-line argument parsing so users pass a PDF path when running the tool.
- Add a welcome banner, page count, and a summarize command for quick document overviews.
- Test the complete tool end-to-end with a real PDF document.
Add command-line argument parsing
Right now, the PDF path is hardcoded as "sample.pdf" inside main(). A real CLI tool should accept the file path as an argument when the user runs it.
Since we are updating main() anyway, we will also add a welcome banner, page count display, a ready message listing available commands, and a handler for the new summarize command. Replace the entire main() function with this updated version.
- In docqa.py, find the existing def main(): function and delete everything from that line down to (and including) the final if __name__ == "__main__": block.
- Replace it with the following code:
def main():
# Check that the user provided a PDF path as an argument
if len(sys.argv) != 2:
print("Usage: python docqa.py <path-to-pdf>")
print("Example: python docqa.py report.pdf")
sys.exit(1)
pdf_path = sys.argv[1]
# Display a welcome banner
print("\n" + "=" * 50)
print(" Document Q&A Tool powered by Claude")
print("=" * 50)
print(f"\nLoading: {pdf_path}")
# Extract text and show page count
document_text = extract_text_from_pdf(pdf_path)
page_count = len(PdfReader(pdf_path).pages)
print(f"Extracted text from {page_count} page(s).")
check_document_size(document_text)
client = Anthropic()
# Show available commands
print("\nReady! Ask questions about your document.")
print("Commands: 'summarize' for a summary, 'quit' to exit.\n")
conversation_history = []
while True:
try:
user_input = input("You: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("\nGoodbye!")
break
if user_input.lower() == "summarize":
print("\nClaude: Generating summary...\n")
summary = generate_summary(client, document_text)
print(summary)
print()
continue
print("\nClaude: ", end="")
answer = ask_claude(client, document_text, conversation_history, user_input)
print(answer)
print()
if __name__ == "__main__":
main()
What does this code do?
This new main() function adds several improvements over the original.
- sys.argv[1] reads the PDF path from the command line. If the user forgets it, the tool prints a usage message and exits cleanly.
- The welcome banner and page count give the user immediate feedback that the tool loaded their file successfully.
- The ready message lists available commands so the user knows they can type summarize or quit.
- The summarize handler calls a generate_summary() function (which we will write next) and then returns to the Q&A loop with continue.
- Save docqa.py.
- You will not test yet because we still need to add the generate_summary() function that main() references.
Add helpful error messages, a welcome banner, and formatting
The summarize command in our loop calls generate_summary(), but that function does not exist yet. This function sends a specialized prompt to Claude asking for a structured breakdown of the document.
- In docqa.py, find the check_document_size function.
- Add the following generate_summary function directly above it (between ask_claude and check_document_size):
def generate_summary(client, document_text):
"""Generate a structured summary of the document."""
system_prompt = f"""You are a document summarization expert. Analyze the following document and provide a structured summary.
DOCUMENT CONTENT:
{document_text}"""
# Ask Claude for a structured summary with specific sections
summary_prompt = """Provide a structured summary of this document with the following sections:
## Overview
(3 sentences maximum)
## Key Points
(Up to 5 bullet points)
## Action Items & Deadlines
(Any tasks, deadlines, or next steps mentioned. Write "None found" if there are none.)
## Important Names, Dates & Figures
(Key people, dates, numbers, or statistics mentioned. Write "None found" if there are none.)"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": summary_prompt}]
)
return response.content[0].text
What does this code do?
- Unlike ask_claude(), this function does not use conversation history. It sends a standalone request because a summary is a one-shot task.
- The summary_prompt defines exact headings that Claude should use in its response. This is a prompt engineering technique called structured output formatting.
- The system_prompt injects the full document text, just like in ask_claude(). The user's question is the formatting template itself.
- Save docqa.py.
Seeing an IndentationError?
Make sure generate_summary is a top-level function (not indented inside another function). It should start at the same indentation level as ask_claude and check_document_size.
help me fix an IndentationError in my docqa.py file
Test the complete flow end-to-end
You need a PDF file to test with. Any PDF will work. If you do not have one handy, search for a short sample PDF online and download it into your docqa folder.
- In your terminal, make sure you are inside the docqa folder.
- Run the tool with your PDF by running this command:
python docqa.py [[PDFNAME="your-file.pdf"]]
What should I see?
You should see the welcome banner with = borders, the file name, a page count, and the ready message with available commands.
- Type a question about your PDF and press Enter. Claude should respond with an answer based on the document content.
- Type summarize and press Enter. You should see a structured summary with Overview, Key Points, Action Items, and Important Names sections.
- Type quit to exit the tool.
- Also test the error handling by running the tool with no arguments:
python docqa.py
- You should see the usage message: Usage: python docqa.py <path-to-pdf>.
Getting a NameError for generate_summary?
Make sure the generate_summary function is defined above main() in the file. Python reads top to bottom, so any function called inside main() must be defined before it.
help me fix a NameError in my Python document Q&A tool
✔️ Awesome, I've got everything!
Great. Double check you have saved docqa.py.
ⓧ I'd like to double check the full code
Here is the complete docqa.py file at this point. Compare yours against it.
import sys
import os
from pypdf import PdfReader
from anthropic import Anthropic
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' not found.")
sys.exit(1)
try:
reader = PdfReader(pdf_path)
except Exception as e:
print(f"Error reading PDF: {e}")
sys.exit(1)
if len(reader.pages) == 0:
print("Error: PDF has no pages.")
sys.exit(1)
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
if not text.strip():
print("Warning: No text could be extracted from this PDF.")
print("The PDF might be scanned or image-based.")
sys.exit(1)
return text
def ask_claude(client, document_text, conversation_history, user_question):
"""Send a question to Claude with document context."""
system_prompt = f"""You are a helpful document analysis assistant. Answer questions based ONLY on the following document content. If the answer is not in the document, say so clearly.
DOCUMENT CONTENT:
{document_text}"""
conversation_history.append({
"role": "user",
"content": user_question
})
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=conversation_history
)
assistant_message = response.content[0].text
conversation_history.append({
"role": "assistant",
"content": assistant_message
})
return assistant_message
def generate_summary(client, document_text):
"""Generate a structured summary of the document."""
system_prompt = f"""You are a document summarization expert. Analyze the following document and provide a structured summary.
DOCUMENT CONTENT:
{document_text}"""
summary_prompt = """Provide a structured summary of this document with the following sections:
## Overview
(3 sentences maximum)
## Key Points
(Up to 5 bullet points)
## Action Items & Deadlines
(Any tasks, deadlines, or next steps mentioned. Write "None found" if there are none.)
## Important Names, Dates & Figures
(Key people, dates, numbers, or statistics mentioned. Write "None found" if there are none.)"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": summary_prompt}]
)
return response.content[0].text
def check_document_size(document_text):
"""Warn if document might be too large for context window."""
estimated_tokens = len(document_text) // 4
if estimated_tokens > 150000:
print(f"\nWarning: This document is very large (~{estimated_tokens:,} estimated tokens).")
print("Claude may not be able to process the entire document at once.")
print("Consider using a shorter document or splitting it into sections.\n")
return False
return True
def main():
if len(sys.argv) != 2:
print("Usage: python docqa.py <path-to-pdf>")
print("Example: python docqa.py report.pdf")
sys.exit(1)
pdf_path = sys.argv[1]
print("\n" + "=" * 50)
print(" Document Q&A Tool powered by Claude")
print("=" * 50)
print(f"\nLoading: {pdf_path}")
document_text = extract_text_from_pdf(pdf_path)
page_count = len(PdfReader(pdf_path).pages)
print(f"Extracted text from {page_count} page(s).")
check_document_size(document_text)
client = Anthropic()
print("\nReady! Ask questions about your document.")
print("Commands: 'summarize' for a summary, 'quit' to exit.\n")
conversation_history = []
while True:
try:
user_input = input("You: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("\nGoodbye!")
break
if user_input.lower() == "summarize":
print("\nClaude: Generating summary...\n")
summary = generate_summary(client, document_text)
print(summary)
print()
continue
print("\nClaude: ", end="")
answer = ask_claude(client, document_text, conversation_history, user_input)
print(answer)
print()
if __name__ == "__main__":
main()
Your document Q&A tool is complete. You have a polished CLI application that can load any PDF, answer questions about it, and generate structured summaries on demand.
Secret mission
Add a "summarize" Command That Generates a Structured Document Summary
When the user types "summarize" instead of a question, the tool generates a structured summary with key points, action items, and important dates extracted from the document. This is the same pattern used in production AI tools to extract structured data from unstructured text.
Clean Up Your Resources
Clean Up Your Resources
This project runs locally on your machine. The only external cost was the Claude API usage during your session, which has already been charged. There are no ongoing costs unless you run the tool again.
Resources you used:
- Project directory with docqa.py on your Desktop.
- Python packages: anthropic and pypdf.
- ANTHROPIC_API_KEY environment variable in your terminal session.
Keep everything running
No action needed. Choose this if you plan to keep using the Document Q&A tool or want to build on it further.
- Your project files stay on your Desktop.
- Your Python packages remain installed and available for other projects.
- Your ANTHROPIC_API_KEY environment variable persists in your shell profile if you added it there.
- Your Anthropic account remains active. You are only charged when you make API requests.
Pause - I'll come back to this later
Free up terminal state while keeping your files intact. Since the API only charges per request, there is nothing to "pause" on the Anthropic side.
- Close any terminal windows running the tool.
- Your project files and installed packages remain ready for next time.
Delete - I don't want to use this again
Remove all project resources from your machine and start fresh.
- Delete your project directory by running this command (macOS/Linux):
rm -rf ~/Desktop/docqa
- On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\docqa
- Uninstall the Python packages by running:
pip uninstall anthropic pypdf -y
- Remove the ANTHROPIC_API_KEY environment variable from your current session by running:
unset ANTHROPIC_API_KEY
- If you added the key to your shell profile (~/.zshrc or ~/.bashrc), open that file and remove the export ANTHROPIC_API_KEY line.
Nice Work!
Nice Work!
You just built an AI-powered document Q&A tool from scratch. You can now chat with any PDF using Claude's API, ask follow-up questions, and get intelligent answers grounded in the document's content.
You've learned how to:
- Built a working CLI tool that extracts text from any PDF and answers questions about it using Claude.
- Integrated the Claude API with system prompts and context injection to ground responses in real document content.
- Implemented multi-turn conversations with conversation history so Claude remembers what you already discussed.
- Secret Mission: Added intelligent document summarization with structured output including key points, action items, and important dates.
Ready to quiz yourself?