Build an AI File Summarizer CLI
Build a Python CLI that uses Gemini to summarize local UTF-8 text files.
Introduction
30 Second Summary
Long notes can hide the few details you need most. Reading every paragraph takes time when you only want the decisions, results, or next steps.
In this project, you will build a Python CLI that turns a local text file into a concise summary. Your tool sends the text to Gemini before showing 3 to 5 useful bullet points in the Terminal.
What You'll Build
A terminal demo that turns fictional campaign notes into concise bullet points you can display immediately or save for later.
By the end of this project, you'll have:
- A working command-line tool that accepts a local text-file path before displaying an AI-generated summary.
- A safer workflow that gives you clear error messages for missing files, directories, unreadable content, and empty files.
- An optional file-output mode that saves the generated summary as UTF-8 text.
- Secret Mission: Add a --style option so the same tool can produce bullet points or one concise paragraph.
Are there any prerequisites?
You need basic Python knowledge. You also need comfort in Terminal.
A Gemini API key plus Python 3.10 or newer complete the prerequisites.
Before We Start
Before the hands-on work begins, choose a UTF-8 text file that matters to you. Naming why a faster summary would help gives the project a concrete purpose.
Set Up the Python Project
Your summarizer combines local Python code with the Gemini API. A system-wide package install could create conflicts with your other Python projects.
A virtual environment keeps the Google GenAI SDK isolated. An environment variable keeps your API key out of the source code.
In this step, get ready to:
- Prepare an isolated project with Python 3.10 or newer.
- Install Google GenAI SDK version 2.33.0.
- Load your Gemini API key into the current shell environment.
Prepare the Python environment
The SDK requires Python 3.10 or newer. You will verify compatibility before creating the isolated environment.
- Press Cmd+Space to open macOS search.
- Type Terminal in the search bar.
- Press Return to open Terminal.
- Create the project folder on your Desktop by running these commands:
cd ~/Desktop
mkdir ai-file-summarizer
cd ai-file-summarizer
What do these commands do?
- The first command moves your terminal to the Desktop.
- The second command creates the ai-file-summarizer folder.
- The final command moves your terminal into that new folder.
- Press Cmd+Space to open macOS search.
- Type Visual Studio Code in the search bar.
- Press Return to open VS Code.
- Click File in the top menu.
- Select Open Folder.
- Select the ai-file-summarizer folder on your Desktop.
- Click Open.
- Return to the Terminal window from earlier.
- Check the installed Python version by running:
python3 --version
Why check the version?
Google GenAI SDK version 2.33.0 requires Python 3.10 or newer. This check prevents an incompatible package installation.
✔️ I see version 3.10 or higher
Your Python version supports the SDK. That compatibility check clears the way for an isolated environment.
ⓧ I see an older version
Your installed Python version is below the SDK requirement. Install the current stable macOS release before continuing.
- Visit the Python.org macOS downloads page.
- Download the current stable macOS installer.
- Run the downloaded installer.
- Return to Terminal after the installation completes.
- Check the version again by running:
python3 --version
What should you see?
The version number should now be 3.10 or newer. Continue once the installed version meets that requirement.
ⓧ Command not found
Your shell cannot find Python 3. Install the current stable macOS release before continuing.
- Visit the Python.org macOS downloads page.
- Download the current stable macOS installer.
- Run the downloaded installer.
- Return to Terminal after the installation completes.
- Check the installation by running:
python3 --version
What should you see?
Terminal should print a Python version of 3.10 or newer. That output confirms the command is available.
A virtual environment gives this project its own Python executable. Packages installed there stay separate from packages used by other projects.
- Create and activate the isolated environment by running these commands:
python3 -m venv .venv
source .venv/bin/activate
What does the virtual environment do?
- The first command creates the environment inside .venv.
- The second command makes that environment active in the current Terminal session.
- The active environment directs Python package installations into this project.
- Confirm which Python executable is active by running:
which python
What does this check prove?
The command shows the exact Python executable your shell uses. A path ending in .venv/bin/python proves the virtual environment is active.
You should see a path ending in .venv/bin/python. Your terminal is now isolated from the system-wide Python installation.
Path does not end in .venv/bin/python?
Make sure Terminal is inside the ai-file-summarizer folder. Activate the environment again if the prompt does not show it as active.
Use this if the active Python path is still unclear: Help me diagnose why my virtual environment is not active.
Install the Google GenAI SDK
A requirements file records the exact dependency needed by the project. Pinning the version makes future installations use the same tested SDK release.
- Return to VS Code.
- Click the New File icon in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Add the pinned dependency by copying this line:
google-genai==2.33.0
What does this dependency line mean?
The package name is google-genai. The double equals sign pins the installation to version 2.33.0.
- Save requirements.txt with Cmd+S.
The first installation may pause while packages download. A quiet moment during this command is normal.
- Install the dependency into the active virtual environment by running:
python -m pip install -r requirements.txt
What does this command do?
- The python -m pip portion uses the package installer linked to the active Python environment.
- The -r requirements.txt portion installs every dependency listed in the file.
- The version pin ensures the project receives Google GenAI SDK 2.33.0.
You should see installation output complete without a package compatibility failure. The SDK now belongs to this project's virtual environment.
Package installation failed?
- Check that the active Python path ends in .venv/bin/python.
- Check that requirements.txt contains the dependency line exactly as shown.
- Check your internet connection if the package download cannot complete.
Use this for targeted help: Help me troubleshoot the SDK installation error. You can also share the error in the NextWork community.
✔️ Awesome, I've got everything!
Great. Save requirements.txt before continuing.
ⓧ I'd like to double check the full code
Compare your dependency file with this complete version.
google-genai==2.33.0
Use this block for comparison. Your saved file should contain the same dependency pin.
Load the Gemini API key
The SDK reads GEMINI_API_KEY from the shell environment when it creates a client. This keeps the credential separate from your Python files.
Credentials deserve extra care. The checks below confirm that the key exists without printing its secret value.
- Check whether the current shell has the key by running:
test -n "$GEMINI_API_KEY" && echo "Gemini key loaded"
What does this check reveal?
The command tests whether GEMINI_API_KEY contains a value. It prints a confirmation without revealing the key.
✔️ I see Gemini key loaded
Your API key is available to the SDK. The secret remains hidden from the Terminal output.
ⓧ The command prints nothing
An empty result means the current shell does not have the key. Store it in your zsh profile so future Terminal sessions can load it.
- Return to VS Code.
- Click File in the top menu.
- Select the option for opening an existing file.
- Enter ~/.zshrc in the file picker.
- Open the .zshrc file.
- Add this export line at the end of the file:
export GEMINI_API_KEY=<YOUR_API_KEY_HERE>
How does this line protect the key?
The export line places the key in your shell environment. Your Python source can use the credential without containing it.
- Replace <YOUR_API_KEY_HERE> with your Gemini API key.
- Save ~/.zshrc with Cmd+S.
- Return to Terminal.
- Reload the zsh profile by running:
source ~/.zshrc
What does reloading do?
Reloading ~/.zshrc applies the export line to the current shell. You can use the key immediately without opening another Terminal window.
- Repeat the safe key check by running:
test -n "$GEMINI_API_KEY" && echo "Gemini key loaded"
What should you see?
Terminal should print Gemini key loaded. The command still keeps the credential value hidden.
Before the final check, do you expect all three commands to confirm the same isolated setup?
- Verify the Python version, active executable, and SDK import by running:
python --version
which python
python -c "from google import genai; print('Google GenAI SDK ready')"
What does the final check prove?
- The first command confirms the active environment uses Python 3.10 or newer.
- The second command confirms the executable comes from .venv/bin/python.
- The third command imports the installed SDK.
You should see Python 3.10 or newer. The next line should end in .venv/bin/python.
The final line should read Google GenAI SDK ready. These three results confirm the project environment is ready.
One of the final checks failed?
- Reactivate .venv if the Python path points outside the project folder.
- Repeat the package installation if the SDK import fails.
- Return to the matching outcome tab if the Python version is too old.
Use this for help with the result you received: Help me diagnose my final Python setup checks.
That is the setup complete. Your isolated Python environment can now run the SDK without exposing the API key in source code.
Next up, you will build the smallest working CLI and prove it can read a real UTF-8 text file.
Build the File-Reading CLI
Your isolated Python environment is ready. The next milestone is a CLI that can accept a real text-file path.
Every future summary depends on reliable file input. This step proves that input flow with repeatable notes stored as UTF-8 text.
In this step, get ready to:
- Create a safe text sample for repeatable tests.
- Build a parser with one positional file argument.
- Run the CLI to prove it can read the full file.
Create a repeatable text sample
A fixed sample gives every run the same input. You can then spot changes in the CLI output without wondering whether the source text changed.
- Select the new-file control in VS Code's file sidebar.
- Name the file sample.txt.
- Add the campaign notes to sample.txt by pasting this text:
The Aurora Cafe team tested two versions of its autumn email campaign. The first version used a general seasonal message, while the second highlighted three specific drinks and included a clear order-ahead button.
The product-focused version generated more menu visits and more completed orders. Customer replies also showed that people liked seeing prices before opening the ordering page. The team noticed that the campaign performed best during the morning commute.
For the next campaign, the team plans to keep the product cards, test a shorter subject line, and schedule delivery earlier in the morning. It will also compare returning customers with first-time buyers instead of reporting only the combined result.
Why use a fixed sample?
Repeatable input makes your tests consistent. These fictional notes also give the CLI enough detail to prove that its file I/O works across several paragraphs.
- Save sample.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Check VS Code's file sidebar for sample.txt.
You should see sample.txt inside the ai-file-summarizer folder.
Can't find the sample file?
- Confirm you created sample.txt inside ai-file-summarizer.
- Save sample.txt again if VS Code shows an unsaved-file indicator.
Help me check why my sample file is missing from the project folder.
Build the file-reading script
A positional argument lets the learner supply a file path directly after the script name. Python's argparse module receives that path for your program.
The Path class turns the supplied value into a path object. That object can read the file with an explicit text encoding.
- Create summarize.py inside ai-file-summarizer using the new-file control in VS Code's file sidebar.
- Build the file-reading CLI in summarize.py by pasting this code:
import argparse
from pathlib import Path
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Print a UTF-8 text file."
)
parser.add_argument(
"file",
type=Path,
help="Path to the UTF-8 text file to read.",
)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
text = args.file.read_text(encoding="utf-8")
print(text)
if __name__ == "__main__":
main()
What does this code do?
- The argparse import provides the command-line parser.
- The Path import represents the supplied file path.
- The build_parser() function defines the CLI interface in one place.
- The positional file argument requires a path whenever the CLI runs.
- The parse_args() method stores the supplied path in args.file.
- The read_text(encoding="utf-8") call reads the file using a predictable encoding.
- The print(text) call sends the file contents to the Terminal.
- Save summarize.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Run the file-reading CLI
Before you run the CLI, consider whether it will print the campaign notes in full.
- Run the CLI against sample.txt by using this command:
python summarize.py sample.txt
What should you see?
You should see all three campaign-note paragraphs in the Terminal. The output should match sample.txt word for word.
That first end-to-end loop works. Your CLI now reads a real file from a supplied path.
The shortfall is intentional. The program repeats every word because it does not summarize the text yet.
Didn't see the campaign notes?
- Confirm the Terminal is still inside the ai-file-summarizer folder.
- Confirm sample.txt sits next to summarize.py.
- Check that both filenames use the same spelling shown in the command.
Help me debug why the file-reading CLI did not print sample.txt.
✔️ Awesome, I've got everything!
Your file-reading CLI works. Double check that sample.txt and summarize.py are saved before moving on.
ⓧ I'd like to double check the full code
Compare these files with the saved files in your ai-file-summarizer folder.
google-genai==2.33.0
What this file controls
The dependency pin keeps every installation on Google GenAI SDK version 2.33.0.
The Aurora Cafe team tested two versions of its autumn email campaign. The first version used a general seasonal message, while the second highlighted three specific drinks and included a clear order-ahead button.
The product-focused version generated more menu visits and more completed orders. Customer replies also showed that people liked seeing prices before opening the ordering page. The team noticed that the campaign performed best during the morning commute.
For the next campaign, the team plans to keep the product cards, test a shorter subject line, and schedule delivery earlier in the morning. It will also compare returning customers with first-time buyers instead of reporting only the combined result.
What this file provides
The fixed campaign notes make every CLI test use the same input. That consistency makes later output changes easy to spot.
import argparse
from pathlib import Path
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Print a UTF-8 text file."
)
parser.add_argument(
"file",
type=Path,
help="Path to the UTF-8 text file to read.",
)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
text = args.file.read_text(encoding="utf-8")
print(text)
if __name__ == "__main__":
main()
What this file does
The script parses one required file path. It reads that file as UTF-8 text before printing the raw contents.
Your CLI can now receive a path and read its text. Next, you will replace raw repetition with a concise AI-generated summary.
Generate an AI Summary
Your CLI can already accept a file path and read the campaign notes. That working data path gives the summarizer a reliable source of text.
The current output repeats every word. In this step, the Google GenAI SDK sends the file text to Gemini for a concise bullet summary.
In this step, get ready to:
- Connect summarize.py to Gemini 3.8 Flash.
- Build a function that requests a factual bullet summary.
- Run the CLI to confirm that Gemini summarizes the campaign notes.
Configure the Gemini connection
The SDK import gives your script access to the Gemini client. A single MODEL constant keeps the model selection visible near the top of the file.
- In summarize.py, select everything from import argparse through return parser.
- Replace the selected code by pasting this version:
import argparse
from pathlib import Path
from google import genai
MODEL = "gemini-3.8-flash"
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Summarize a UTF-8 text file with Gemini."
)
parser.add_argument(
"file",
type=Path,
help="Path to the UTF-8 text file to summarize.",
)
return parser
What Changed in the Setup?
- The from google import genai import makes the installed SDK available to your script.
- The MODEL constant selects the stable gemini-3.8-flash model.
- The updated parser description tells learners that the CLI summarizes its input with Gemini.
- The updated help text explains that the positional path identifies the file to summarize.
- Save summarize.py.
- Confirm that the file still loads by running this command:
python summarize.py sample.txt
What Does This Check Prove?
You should still see all three campaign paragraphs in the Terminal. This proves that the SDK import loads successfully without breaking the existing file-reading path.
Having Trouble Loading the SDK?
- Confirm that the virtual environment remains active in the integrated terminal.
- Check that requirements.txt still contains google-genai==2.33.0.
- Check that the import reads from google import genai exactly.
Help me diagnose why the Google GenAI SDK does not load in my active virtual environment.
Build the summarization function
A prompt defines the format and factual boundaries for the response. The Interactions API sends that prompt to the selected model.
- In summarize.py, place your cursor below return parser.
- Add the summarize_text() function by pasting this code:
def summarize_text(text: str) -> str:
prompt = (
"Summarize the following text in 3 to 5 concise bullet points. "
"Preserve the main ideas and do not add facts.\n\n"
f"{text}"
)
client = genai.Client()
try:
interaction = client.interactions.create(
model=MODEL,
input=prompt,
store=False,
)
if not interaction.output_text:
raise RuntimeError("The model returned no text.")
return interaction.output_text
finally:
client.close()
What Does This Function Do?
- The prompt requests three to five concise bullet points.
- The factual boundary tells Gemini to preserve the source ideas without adding claims.
- The genai.Client() call creates a client using the loaded API key.
- The store=False argument makes this a stateless interaction.
- The interaction.output_text property provides the generated summary.
- The finally block closes the client even when the request fails.
- Save summarize.py.
- Check that the script remains runnable by running this command:
python summarize.py sample.txt
What Should You See?
The Terminal still prints the complete campaign notes because main() currently follows the raw-text path. This run confirms that the new function loads without a syntax error.
Does the Script Stop Before Printing?
- Compare the indentation inside summarize_text() with the code above.
- Confirm that summarize_text() sits above main().
- Check that the prompt contains matching parentheses and quotation marks.
Help me find the syntax or indentation problem in my summarize_text function.
Connect the summary to the CLI
The main() function controls the CLI data flow. It now needs to pass the loaded text into summarize_text() before printing the result.
- In summarize.py, select the existing main() function from def main() -> None: through print(text).
- Replace the selected function by pasting this version:
def main() -> None:
parser = build_parser()
args = parser.parse_args()
text = args.file.read_text(encoding="utf-8")
summary = summarize_text(text)
print(summary)
How Does the Data Flow Now?
- The file path still comes from the positional CLI argument.
- The read_text() call loads the UTF-8 campaign notes.
- The summarize_text(text) call sends those notes to Gemini.
- The print(summary) call displays the generated response.
- Save summarize.py.
Before you run the CLI, do you expect the Terminal to repeat the campaign notes or compress them into a few points?
- Generate the AI summary by running this command:
python summarize.py sample.txt
What Should You See?
You should see three to five concise bullet points instead of the original paragraphs. The wording can vary between requests because Gemini generates each response.
Strong result. Your CLI now turns a local text file into a useful AI summary.
Did the Summary Request Fail?
- Confirm that the .venv environment remains active in the same terminal.
- Confirm that GEMINI_API_KEY remains loaded in the terminal session.
- Compare the model value with gemini-3.8-flash.
Help me troubleshoot why my Gemini summary request fails when I run the file summarizer CLI.
✔️ Awesome, I've got everything!
Your saved summarize.py now reads the file and prints a Gemini-generated bullet summary.
ⓧ I'd like to double check the full code
This reference shows the finished summarize.py for this step.
import argparse
from pathlib import Path
from google import genai
MODEL = "gemini-3.8-flash"
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Summarize a UTF-8 text file with Gemini."
)
parser.add_argument(
"file",
type=Path,
help="Path to the UTF-8 text file to summarize.",
)
return parser
def summarize_text(text: str) -> str:
prompt = (
"Summarize the following text in 3 to 5 concise bullet points. "
"Preserve the main ideas and do not add facts.\n\n"
f"{text}"
)
client = genai.Client()
try:
interaction = client.interactions.create(
model=MODEL,
input=prompt,
store=False,
)
if not interaction.output_text:
raise RuntimeError("The model returned no text.")
return interaction.output_text
finally:
client.close()
def main() -> None:
parser = build_parser()
args = parser.parse_args()
text = args.file.read_text(encoding="utf-8")
summary = summarize_text(text)
print(summary)
if __name__ == "__main__":
main()
How Is the File Organized?
- The imports provide CLI parsing, file paths, and the Gemini client.
- The MODEL constant selects the model endpoint.
- The build_parser() function defines the positional file argument.
- The summarize_text() function creates the prompt and returns the model output.
- The main() function connects file reading to summary generation.
Your CLI now replaces raw repetition with an AI-generated summary. Next up, you will add helpful file errors and an option for saving the result.
Handle Errors and Save Summaries
Your summarizer already turns sample.txt into useful bullets. A portfolio-ready CLI also needs to guide someone when a file cannot be read.
This step adds error handling for file failures. It also lets someone save the generated summary without copying text from the Terminal.
In this step, get ready to:
- Turn common file failures into clear CLI errors.
- Route Gemini failures through the argument parser.
- Print summaries or save them to a chosen output file.
Catch invalid file inputs
The current CLI reads the supplied path directly. A missing file therefore escapes as a Python traceback instead of a useful command-line message.
Before you run this, do you expect the current CLI to explain the problem cleanly or expose its internal failure?
- Test the current failure path by running this command:
python summarize.py missing.txt
The Traceback Is the Problem
You should see a Python traceback because the file read raises an exception. This designed shortfall proves the CLI needs a boundary between low-level file failures and the person running the command.
A dedicated file-reading function can check the path before reading it. It can also translate decoding problems into consistent messages that the parser can display later.
- In summarize.py, place read_text_file() between build_parser() and summarize_text() by pasting this code:
def read_text_file(path: Path) -> str:
if not path.exists():
raise ValueError(f"File not found: {path}")
if not path.is_file():
raise ValueError(f"Not a file: {path}")
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError as exc:
raise ValueError("Input must be a UTF-8 text file.") from exc
except OSError as exc:
raise ValueError(f"Could not read {path}: {exc}") from exc
if not text.strip():
raise ValueError("Input file is empty.")
return text
What Does This Helper Check?
- The path.exists() check catches a missing path before Python tries to read it.
- The path.is_file() check rejects a directory supplied in place of a file.
- The first exception handler converts invalid UTF-8 input into a focused message.
- The second exception handler converts other operating system read failures into a ValueError.
- The text.strip() check rejects a file containing no meaningful text.
- Save summarize.py.
- Confirm the existing summary path still runs by running this command:
python summarize.py sample.txt
What Should You See?
You should still see a three-to-five-bullet summary of the campaign notes. This confirms that Python can load the new helper without a syntax failure.
Did the Working Summary Stop?
Check that read_text_file() sits outside every other function. Match the indentation shown in the code block.
Confirm that the except blocks align with try.
Help me fix the new file-reading helper.
Add optional output and route failures
An optional output path gives the caller control over where the summary goes. Argument parsing converts that supplied path into a Path object before the main flow uses it.
- In summarize.py, replace the current build_parser() function with this version:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Summarize a UTF-8 text file with Gemini."
)
parser.add_argument(
"file",
type=Path,
help="Path to the UTF-8 text file to summarize.",
)
parser.add_argument(
"-o",
"--output",
type=Path,
help="Optional path for saving the summary.",
)
return parser
What Does the Output Argument Add?
The caller can use -o or --output to supply a destination. The parser stores that destination as args.output.
The argument remains optional. A command without it keeps the Terminal output used earlier.
- Save summarize.py.
- Confirm the default command remains valid by running:
python summarize.py sample.txt
What Should You See?
You should see the campaign summary in the Terminal. The optional argument leaves the existing command unchanged.
Is the Parser Rejecting the Command?
Check the commas after each argument setting in build_parser(). Confirm that both output flags sit inside the same parser.add_argument() call.
Help me debug the optional output argument.
The parser can now capture an output path. The main flow needs to apply the file checks before the API call and choose between printing or saving the result.
- In summarize.py, replace the current main() function with this version:
def main() -> None:
parser = build_parser()
args = parser.parse_args()
try:
text = read_text_file(args.file)
except ValueError as exc:
parser.error(str(exc))
try:
summary = summarize_text(text)
except Exception as exc:
parser.error(f"AI request failed: {exc}")
if args.output:
try:
args.output.write_text(summary + "\n", encoding="utf-8")
except OSError as exc:
parser.error(f"Could not write {args.output}: {exc}")
print(f"Summary saved to {args.output}")
else:
print(summary)
How Does the Main Flow Stay Safe?
- The first try block sends file-related ValueError messages through parser.error().
- The second try block turns an AI request failure into a CLI error.
- The output branch writes the summary with UTF-8 encoding when args.output contains a path.
- The final else branch prints the summary when no output path was supplied.
- Save summarize.py.
Your completed summarize.py should now match the reference below.
✔️ Awesome, I've got everything!
Your file now contains input checks, clean failure routing, and optional saved output.
ⓧ I'd like to double check the full code
import argparse
from pathlib import Path
from google import genai
MODEL = "gemini-3.8-flash"
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Summarize a UTF-8 text file with Gemini."
)
parser.add_argument(
"file",
type=Path,
help="Path to the UTF-8 text file to summarize.",
)
parser.add_argument(
"-o",
"--output",
type=Path,
help="Optional path for saving the summary.",
)
return parser
def read_text_file(path: Path) -> str:
if not path.exists():
raise ValueError(f"File not found: {path}")
if not path.is_file():
raise ValueError(f"Not a file: {path}")
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError as exc:
raise ValueError("Input must be a UTF-8 text file.") from exc
except OSError as exc:
raise ValueError(f"Could not read {path}: {exc}") from exc
if not text.strip():
raise ValueError("Input file is empty.")
return text
def summarize_text(text: str) -> str:
prompt = (
"Summarize the following text in 3 to 5 concise bullet points. "
"Preserve the main ideas and do not add facts.\n\n"
f"{text}"
)
client = genai.Client()
try:
interaction = client.interactions.create(
model=MODEL,
input=prompt,
store=False,
)
if not interaction.output_text:
raise RuntimeError("The model returned no text.")
return interaction.output_text
finally:
client.close()
def main() -> None:
parser = build_parser()
args = parser.parse_args()
try:
text = read_text_file(args.file)
except ValueError as exc:
parser.error(str(exc))
try:
summary = summarize_text(text)
except Exception as exc:
parser.error(f"AI request failed: {exc}")
if args.output:
try:
args.output.write_text(summary + "\n", encoding="utf-8")
except OSError as exc:
parser.error(f"Could not write {args.output}: {exc}")
print(f"Summary saved to {args.output}")
else:
print(summary)
if __name__ == "__main__":
main()
How to Use This Reference
Compare the function order and indentation with your file. Keep requirements.txt and sample.txt unchanged.
Verify errors and saved output
The final checks exercise both branches of the CLI. One command proves that a missing input fails cleanly. The other proves that a valid summary can be reused as a file.
Before you run the missing-file check, do you expect another traceback or a short usage error?
- Test the missing-file path by running:
python summarize.py missing.txt
What Should You See?
You should see a short usage error containing File not found: missing.txt. You should not see a Python traceback.
Still Seeing a Traceback?
Confirm that main() calls read_text_file(args.file) inside the first try block.
Check that the matching except ValueError as exc block calls parser.error(str(exc)).
Help me remove the traceback from the missing-file path.
Before you save the next summary, what do you expect the Terminal to show after each command?
- Save a generated summary to summary.txt by running the first command below.
- Print the saved file by running the second command below.
python summarize.py sample.txt -o summary.txt
cat summary.txt
What Should You See?
The first command should print Summary saved to summary.txt. The second command should print the generated bullet summary stored in summary.txt.
Was the Summary Not Saved?
Check that the output branch uses args.output.write_text() inside its try block. Confirm that summary.txt appears inside the ai-file-summarizer folder.
Help me debug the saved-summary path.
That is the practical upgrade complete. Your CLI now protects people from common failures and produces a summary they can keep.
Secret mission
Secret Mission: Add a Summary Style Option
Give your summarizer two useful output formats. Add a controlled style option that produces concise bullet points or one concise paragraph while preserving the existing file handling and Gemini request flow.
Clean Up Your Resources
Clean Up Your Resources
Your project has no ongoing idle cost because its files stay on your Mac and Gemini API calls happen only when you run the CLI. Decide whether to keep the project active, pause its environment, or delete its local resources.
Resources you used:
- The ai-file-summarizer folder containing your source files and .venv environment.
- The GEMINI_API_KEY export line in ~/.zshrc, if you added it solely for this project.
Keep everything running
No action is needed. Choose this option if you plan to keep improving the CLI or summarizing more files.
- Leave the ai-file-summarizer folder in its current location.
- Keep the GEMINI_API_KEY export line in ~/.zshrc if you plan to use the Gemini API in future projects.
- Continue using the active .venv environment for this project.
Pause - I'll come back to this later
Pausing ends the current virtual environment session. Your project files and API key configuration remain available for later.
- Pause the active .venv by running this command:
deactivate
What does this command do?
This exits the virtual environment in your current Terminal session. Your .venv folder stays on disk, so your project is now paused without losing its installed package.
- Reactivate the saved environment when you return by running:
source .venv/bin/activate
How does reactivation work?
This reconnects your shell to the existing .venv environment. You can continue using the project without reinstalling its dependency.
Delete - I don't want to use this again
Deleting the folder is final. It removes the local source files and the isolated environment created for this project.
- Deactivate the active .venv by running:
deactivate
Why deactivate first?
This returns your shell to its regular Python environment before the project folder is removed. The files remain intact until you run the deletion command.
Your API key may support other projects, so its shell setting deserves a quick check before removal. Delete the setting only if you added it solely for this CLI.
- Switch back to VS Code from earlier if you added the key setting solely for this project.
- Use the file-opening control in VS Code to open ~/.zshrc.
- Find the line that exports GEMINI_API_KEY.
- Delete that export line.
- Save ~/.zshrc.
- Confirm that ~/.zshrc no longer contains GEMINI_API_KEY.
- Move out of the ai-file-summarizer folder by running:
cd ..
Why move out of the folder?
This moves Terminal into the directory that contains ai-file-summarizer. The project folder can now be removed as one complete unit.
- Confirm that the project folder is listed by running:
ls
What should you see?
You should see ai-file-summarizer in the output. This confirms that Terminal is in the correct location for deletion.
Deletion Is Permanent
The next command permanently removes the local project folder. Any files stored only inside that folder disappear with it.
- Copy any summary you want to preserve outside ai-file-summarizer.
- Delete the project folder by running:
rm -rf ai-file-summarizer
What does this command remove?
This removes the complete ai-file-summarizer folder. Its source files and .venv environment are deleted together.
- Verify that the folder is gone by running:
ls
What should you see now?
Your local cleanup is complete. You should no longer see ai-file-summarizer in the output.
Nice Work!
Nice Work!
You did it. Your Python CLI now turns local UTF-8 text files into concise Gemini summaries that you can print or save.
What you learned:
- Built a Python CLI with argparse that accepts a local text-file path. The result can print in the Terminal or save to a chosen output file.
- Added UTF-8 file handling with pathlib. Missing paths now produce clear CLI errors. Empty files get the same safe treatment.
- Connected the Google GenAI SDK to the Gemini Interactions API. Your prompt requests concise summaries without adding facts.
- Completed the Secret Mission by adding a controlled --style option for bullet or paragraph summaries.
Ready to quiz yourself?