Build a Course Handbook AI Agent
Build a Gemini agent that searches a course handbook through a FastAPI API.
Introduction
30 Second Summary
Course rules are easy to misremember when the details sit inside a long handbook. A confident guess can send a learner in the wrong direction.
In this project, you will build a local AI agent that lets Gemini choose when to search a fictional course handbook. Every policy answer will include the handbook section that supplied its evidence.
What You'll Build
When you ask about resubmissions in an interactive browser page, your assistant returns the fictional rule with Resubmissions named as its source.
By the end of this project, you'll have:
- A sourced policy answer for Can I submit an assessment again? that uses semantic search to retrieve the fictional Resubmissions rule.
- A custom function calling loop that reports its choice through tool_used. Policy questions show true. A greeting shows false.
- An interactive FastAPI page at http://127.0.0.1:5000/docs for sending questions through the local endpoint. Every response displays answer, tool_used, and sources.
- Secret Mission: Add an Equipment Loans policy to the handbook. The agent must retrieve it from a paraphrased question without changes to the Python code.
Are there any prerequisites?
You'll need a Windows computer with internet access plus a Google account that can access Google AI Studio.
No coding setup is assumed.
Before We Start
Your course assistant needs a clear boundary before you build it. This is your moment to define what it answers directly and when it must search the approved fictional handbook.
Set Up Windows and Gemini Access
Before your course assistant can contact the Gemini API, it needs a stable Windows workspace. A mismatched runtime can stop the pinned packages from installing.
A leaked API key could give someone else access to your Gemini account. You will store this credential outside your project files.
This step prepares Visual Studio Code, the Microsoft Python extension, and Python for the build. You will also create an isolated environment with the exact package versions used throughout the project.
In this step, get ready to:
- Install the pinned Windows development tools.
- Create Gemini access without exposing the API key in source code.
- Prepare an isolated Python environment with the pinned packages.
Install and verify the desktop tools
Visual Studio Code keeps your files and terminal in one workspace. The Microsoft Python extension connects that workspace to the interpreter you select.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter if Visual Studio Code appears.
If the editor opens, its version determines which path you need. If Windows cannot find the editor, use the installation path below.
- Select Help from the Visual Studio Code top menu if the editor opened.
- Select About.
- Read the version shown in the dialog.
Choose the tab that matches what you found.
✔️ The pinned VS Code version is installed
The About dialog reports 1.136.2. Your editor matches the project environment.
- Close the About dialog.
ⓧ I see an older version
An older editor can behave differently from the version used in this project. Update it before installing the Python extension.
- Open the official Visual Studio Code Windows setup guide.
- Download the Windows User setup.
- Close Visual Studio Code.
- Run the downloaded installer.
- Approve the Windows permission request if one appears.
- Keep the installer defaults.
- Complete the installation.
The updated editor is now available through Windows search.
- Press the Windows key.
- Type Visual Studio Code into the search field.
- Press Enter to open the editor.
- Select Help from the top menu.
- Select About.
You should see 1.136.2 in the version details.
ⓧ VS Code is not installed
The Windows User setup installs Visual Studio Code inside your Windows account. This path is recommended for most learners.
- Open the official Visual Studio Code Windows setup guide.
- Download the Windows User setup.
- Run the downloaded installer.
- Approve the Windows permission request if one appears.
- Keep the installer defaults.
- Complete the installation.
Visual Studio Code is now available through Windows search.
- Press the Windows key.
- Type Visual Studio Code into the search field.
- Press Enter to open the editor.
- Select Help from the top menu.
- Select About.
You should see 1.136.2 in the version details.
Your editor is ready. The next addition gives it Python language support.
- Close the About dialog.
- Select the Extensions icon in the left activity bar.
- Enter ms-python.python in the extension search field.
The search results should include the Python extension published by Microsoft. Checking the publisher protects you from similarly named extensions.
- Select the Python extension published by Microsoft.
- Click Install.
You should see an installed state on the extension page. Visual Studio Code can now discover Python interpreters.
PowerShell provides the Windows terminal used by this project. The first command checks whether the required Python runtime is already available.
- Press the Windows key to open Windows search.
- Type PowerShell into the search field.
- Press Enter to open a PowerShell terminal.
- Check the installed Python version by running this command:
python --version
What does this command check?
The command asks the Python installation available to PowerShell for its version. The result shows whether your computer has the pinned runtime.
Choose the tab that matches the command output.
✔️ I see the required Python version
PowerShell reports Python 3.14.7. This runtime can create the project environment.
ⓧ I see an older version
An older runtime may reject the pinned dependencies. Install the stable 3.14.7 release before creating the project environment.
- Open the official Python downloads for Windows.
- Download the stable Python 3.14.7 Windows release.
- Run the downloaded installer.
- Enable the installer option that makes Python available from the terminal.
- Approve the Windows permission request if one appears.
- Complete the installation.
A fresh terminal session picks up the updated Python command.
- Close the current PowerShell terminal.
- Press the Windows key.
- Type PowerShell into the search field.
- Press Enter to open a new terminal.
- Check the updated Python version by running this command:
python --version
What should you see?
PowerShell should report Python 3.14.7. The new terminal can now find the required runtime.
ⓧ Command not found
Windows cannot currently find a Python installation. Installing the stable release provides the runtime used by this project.
- Open the official Python downloads for Windows.
- Download the stable Python 3.14.7 Windows release.
- Run the downloaded installer.
- Enable the installer option that makes Python available from the terminal.
- Approve the Windows permission request if one appears.
- Complete the installation.
A new terminal session lets Windows discover the installed runtime.
- Press the Windows key.
- Type PowerShell into the search field.
- Press Enter to open a new terminal.
- Confirm the installation by running this command:
python --version
What should you see?
PowerShell should report Python 3.14.7 without a missing-command error.
Still blocked by the desktop tools?
Close every PowerShell window after installing Python. A new terminal session is required before Windows can find the updated command path.
Confirm that the Python extension lists Microsoft as its publisher. Help me troubleshoot my Windows development tools. You can also share the non-secret error text with the NextWork community.
Create Gemini access securely
Google AI Studio creates the authorization key that your local code uses to access Gemini. The Google Gen AI client reads that key from a Windows environment variable.
The key is a live credential, so this part can feel sensitive. You will move it directly into Windows without placing it in a Python file or screenshot.
- Open Google AI Studio in your browser.
- Sign in with the Google account you want to use for this project.
- Select the option for creating an API key.
- Choose a Google Cloud project if prompted.
- Create the authorization API key.
- Copy the key from the reveal screen.
Why use a Windows environment variable?
The Google Gen AI client automatically detects GEMINI_API_KEY when it starts. Your future Python files can authenticate without containing the credential.
Keeping the key outside source code prevents it from entering files that you might share later. The credential stays separate from the project.
- Press the Windows key to open Windows search.
- Type Environment Variables into the search field.
- Select the result for editing environment variables for your account.
You should see the Windows controls for user environment variables. This area stores values for your Windows account.
- Create a new entry under User variables.
- Enter GEMINI_API_KEY as the variable name.
- Paste the authorization key as the variable value.
- Save the new user variable.
You should see GEMINI_API_KEY listed under your user variables. The variable name confirms that the credential is stored where the Gemini client expects it.
- Close the environment settings.
- Close the API key reveal in Google AI Studio.
- Close the existing PowerShell terminal.
- Open a new PowerShell terminal through Windows search.
Can't save the environment variable?
Make sure you create the entry under User variables. This location does not require a system-wide variable.
Never paste the key into a troubleshooting message. Help me set up GEMINI_API_KEY safely. You can also describe the non-secret screen state to the NextWork community.
Create the isolated project environment
A virtual environment keeps this project's packages separate from other Python projects. The pinned requirements make later commands use the same library versions.
- Return to Visual Studio Code.
- Select File from the top menu.
- Select Open Folder.
You should see the Windows folder picker. This is where you choose the project location.
- Select your Desktop in the folder picker.
- Create a folder named course-handbook-agent.
The new course-handbook-agent folder should appear on your Desktop.
- Select the course-handbook-agent folder.
- Confirm the folder selection.
You should see course-handbook-agent at the top of the Explorer sidebar. Every project file now has one predictable home.
- Select the new-file control in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Add the pinned project packages by pasting this content into requirements.txt:
fastapi==0.141.1
uvicorn==0.52.4
google-genai==2.23.0
pydantic==2.13.5
What do these packages provide?
- FastAPI provides the HTTP API you expose later in the project.
- Uvicorn runs that API on your Windows computer.
- Google Gen AI Python SDK connects your code to Gemini generation and embedding models.
- Pydantic validates the questions and responses crossing the API boundary.
- Save requirements.txt by pressing Ctrl+S.
- Confirm that requirements.txt appears in the Explorer sidebar.
The saved file records all four pinned package versions. The installation command reads directly from this list.
Does the file name look different?
Check that the Explorer sidebar shows exactly requirements.txt. Remove any extra .txt suffix before continuing.
Help me check my requirements file.
✔️ Awesome, I've got everything!
Your pinned dependency file is saved inside course-handbook-agent.
ⓧ I'd like to double check the full code
Compare your complete requirements.txt file with this reference.
fastapi==0.141.1
uvicorn==0.52.4
google-genai==2.23.0
pydantic==2.13.5
What should match?
Each package name must match the reference. Each version must also match exactly.
- Select Terminal from the Visual Studio Code top menu.
- Select New Terminal.
- Create the project virtual environment by running this command:
python -m venv .venv
What does this command create?
The command creates an isolated Python environment in the .venv folder. Windows places the environment's interpreter inside its Scripts directory.
Direct interpreter paths let you use the environment without activating it. This avoids PowerShell execution-policy problems.
- Check the Explorer sidebar for the new .venv folder.
You should see .venv inside course-handbook-agent. That folder confirms the isolated environment exists.
Don't see the virtual environment?
Confirm that the terminal is inside the open course-handbook-agent folder. A missing Python command means the earlier runtime installation still needs attention.
Help me create the virtual environment. You can also share the non-secret output with the NextWork community.
- Install the pinned packages into the virtual environment by running this command:
.venv\Scripts\python.exe -m pip install -r requirements.txt
Why use the environment interpreter directly?
The command runs pip through the interpreter inside .venv. The environment does not need to be activated first.
The installer reads the exact versions from requirements.txt. It also adds the Python and Uvicorn executables to the environment's Scripts directory.
When installation succeeds, PowerShell returns to the prompt without an installation error. The four packages now belong to this project environment.
Did package installation fail?
Confirm that requirements.txt contains the four exact pins. Correct any changed package name or version before running the installation again.
Help me fix my package installation. You can also share the non-secret installation output with the NextWork community.
- Press Ctrl+Shift+P to open the Visual Studio Code Command Palette.
- Enter Python: Select Interpreter.
- Select the interpreter at .venv\Scripts\python.exe.
The Visual Studio Code status bar should now identify the .venv interpreter. Editor features now use the same environment as your terminal commands.
Before you run the final check, which Python version and four package versions do you expect the virtual environment to report?
- Verify the isolated interpreter and pinned packages by running these commands:
.venv\Scripts\python.exe --version
.venv\Scripts\python.exe -m pip show fastapi google-genai uvicorn pydantic
What should you see?
The first result should report Python 3.14.7. The package details should report FastAPI 0.141.1 and Google Gen AI Python SDK 2.23.0.
The remaining package details should report Uvicorn 0.52.4 and Pydantic 2.13.5. These results prove that PowerShell is using the pinned environment.
Seeing a missing package or wrong version?
Confirm that every line in requirements.txt matches the full-code reference. Run the installation command again after correcting any mismatch.
Use the interpreter inside the .venv\Scripts directory for each check. Help me fix my pinned Python environment. You can also share the non-secret package output with the NextWork community.
Your Windows workspace now has the pinned tools, an isolated environment, and secure Gemini access. Next, you will send the assistant its first policy question and expose the knowledge it is still missing.
Meet Gemini and Its Knowledge Limit
Your pinned Windows environment is ready. Your selected virtual environment can now call Gemini through the Google Gen AI Python SDK.
A general model can communicate without holding your private course rules. Safe policy answers need approved handbook evidence.
In this step, you will send a fictional resubmission question. The run turns that question into a visible test of the model's current knowledge.
In this step, get ready to:
- Create hello_agent.py with a Gemini client plus a policy guard.
- Send a resubmission question through the Interactions API.
- Inspect the response for the model's knowledge boundary.
Create the guarded Gemini assistant
A system instruction defines how the assistant handles missing policy knowledge. This instruction tells Gemini to admit when handbook evidence is unavailable.
- In the VS Code Explorer sidebar, use the file creation control beside the course-handbook-agent folder.
- Name the new file hello_agent.py.
You'll see hello_agent.py listed beside requirements.txt in the course-handbook-agent folder.
- Add the guarded Gemini interaction by pasting this code into hello_agent.py:
from google import genai
MODEL = "gemini-3.8-flash"
client = genai.Client()
interaction = client.interactions.create(
model=MODEL,
system_instruction=(
"You are a course assistant without access to the course handbook. "
"Never guess course rules. If asked about a course policy, say that "
"you do not have handbook access yet."
),
input="Can I resubmit an assessment?",
)
print(f"Agent: {interaction.output_text}")
What does this code do?
- The genai import gives your script access to the installed SDK.
- The MODEL constant keeps the script on gemini-3.8-flash.
- The genai.Client() client reads the existing GEMINI_API_KEY environment variable.
- The system_instruction parameter prevents unsupported policy answers.
- The client.interactions.create() call sends the resubmission question to Gemini.
- The interaction.output_text value provides the response printed in the terminal.
- Save hello_agent.py.
- Check the course-handbook-agent folder in the Explorer sidebar.
You should see hello_agent.py stored inside the folder.
Seeing an Import Warning?
- Check the Python interpreter shown in VS Code.
- Select .venv\Scripts\python.exe again if VS Code shows another interpreter.
Help me fix the Gemini connection safely.
Run the private-policy check
The script asks for a specific fictional course rule. Running it tests whether the model follows its instruction when no handbook content is present.
Before you run the script, consider this question: will Gemini state a resubmission rule or stop at the handbook boundary.
- Run the policy check in the existing VS Code PowerShell terminal by using this command:
.venv\Scripts\python.exe hello_agent.py
The terminal output begins with Agent:. You'll see a response explaining that Gemini cannot access the course handbook yet.
The response withholds the fictional resubmission rule. That shortfall is intentional.
That is a meaningful first win: your assistant can call Gemini without inventing a private policy.
Script Not Reaching Gemini?
- Save hello_agent.py if the terminal is running an older copy.
- Close the current VS Code terminal if authentication fails.
- Create a fresh terminal so Windows reloads the existing GEMINI_API_KEY user variable.
- Run the policy check again with the command above.
Help me diagnose the failed Gemini request without exposing my API key.
✔️ Awesome, I've got everything!
Your hello_agent.py file is saved. The terminal result confirms that Gemini respects the handbook-access boundary.
ⓧ I'd like to double check the full code
The complete hello_agent.py reference is shown below.
from google import genai
MODEL = "gemini-3.8-flash"
client = genai.Client()
interaction = client.interactions.create(
model=MODEL,
system_instruction=(
"You are a course assistant without access to the course handbook. "
"Never guess course rules. If asked about a course policy, say that "
"you do not have handbook access yet."
),
input="Can I resubmit an assessment?",
)
print(f"Agent: {interaction.output_text}")
Your first Gemini interaction now exposes its knowledge boundary. Next, you'll build the semantic retrieval layer that supplies approved handbook evidence.
Build the Semantic Handbook Retriever
Your Gemini assistant correctly refused to invent a resubmission policy. It now needs approved evidence before it can answer that question.
In this step, you will build a semantic retrieval system that finds relevant handbook sections from meaning. This lets the retriever connect a paraphrased question with the matching policy.
In this step, get ready to:
- Create a fictional course handbook with eight policy sections.
- Convert each handbook section into an embedding for semantic comparison.
- Rank the two strongest matches with cosine similarity.
Create the fictional handbook
The retriever needs a trusted knowledge source. Each handbook heading becomes a source label that the finished agent can show alongside its answer.
- Return to the Explorer sidebar in Visual Studio Code.
- Create a file named course_handbook.txt inside the course-handbook-agent folder.
- Add the fictional handbook policies by copying this content into course_handbook.txt:
## Assignment Deadlines
Assignments are due every Friday at 5:00 PM in the learner's local time zone. The submission portal records the final upload time.
## Late Submissions
Work submitted after the deadline loses 10 percent of the available marks per calendar day. Work more than three calendar days late is not accepted unless an extension was approved before the deadline.
## Attendance
Learners must attend at least 80 percent of live workshops. Approved absences do not reduce the attendance percentage when supporting evidence is submitted within five calendar days.
## Grading
The final grade is calculated from projects worth 60 percent, quizzes worth 25 percent, and participation worth 15 percent. A final score of 70 percent is required to pass.
## Resubmissions
Each learner may resubmit one failed assessment once. The revised submission is due within seven calendar days of receiving feedback, and the revised grade is capped at 80 percent.
## Office Hours
Instructor office hours run online every Tuesday from 4:00 PM to 5:00 PM. Learners should reserve a slot through the course portal.
## Learning Support
Learners who need study support should open a Learning Support request in the course portal. The support team replies within two business days.
## Academic Integrity
Submitted work must be the learner's own. External sources and AI-assisted material must be acknowledged according to the citation guide in the course portal.
How is the handbook structured?
- Each line beginning with ## names a source that the retriever can return.
- Each paragraph beneath a heading contains the fictional policy evidence for that source.
- Each blank line separates one complete section from the next.
- Save course_handbook.txt.
- Confirm that the Visual Studio Code Explorer lists course_handbook.txt beside hello_agent.py.
Handbook file missing?
Check that course_handbook.txt sits directly inside course-handbook-agent. A file nested inside another folder will not match the path used by the retriever.
Check that the filename ends with .txt. Windows can hide known file extensions.
Help me check my handbook file location.
✔️ Awesome, I've got everything!
Your fictional handbook is saved with eight separate policy sections.
ⓧ I'd like to double check the full code
Compare your course_handbook.txt file with this complete version:
## Assignment Deadlines
Assignments are due every Friday at 5:00 PM in the learner's local time zone. The submission portal records the final upload time.
## Late Submissions
Work submitted after the deadline loses 10 percent of the available marks per calendar day. Work more than three calendar days late is not accepted unless an extension was approved before the deadline.
## Attendance
Learners must attend at least 80 percent of live workshops. Approved absences do not reduce the attendance percentage when supporting evidence is submitted within five calendar days.
## Grading
The final grade is calculated from projects worth 60 percent, quizzes worth 25 percent, and participation worth 15 percent. A final score of 70 percent is required to pass.
## Resubmissions
Each learner may resubmit one failed assessment once. The revised submission is due within seven calendar days of receiving feedback, and the revised grade is capped at 80 percent.
## Office Hours
Instructor office hours run online every Tuesday from 4:00 PM to 5:00 PM. Learners should reserve a slot through the course portal.
## Learning Support
Learners who need study support should open a Learning Support request in the course portal. The support team replies within two business days.
## Academic Integrity
Submitted work must be the learner's own. External sources and AI-assisted material must be acknowledged according to the citation guide in the course portal.
What should match?
Check every heading and policy paragraph. The blank lines between sections allow read_sections() to separate the handbook correctly.
Embed the handbook sections
An embedding represents the meaning of text as a list of numbers. Similar questions and policies produce vectors that can be compared mathematically.
Why use an in-memory index?
This handbook contains only eight short sections. An in-memory index keeps the retrieval process visible without adding a database.
The index is built once during each run. Later searches reuse the cached document vectors.
- Create a file named retriever.py inside the course-handbook-agent folder.
- Add the imports and embedding configuration by copying this first block into retriever.py:
import json
from math import sqrt
from pathlib import Path
from google import genai
from google.genai import types
EMBEDDING_MODEL = "gemini-embedding-2"
EMBEDDING_DIMENSIONS = 768
HANDBOOK_PATH = Path(__file__).with_name("course_handbook.txt")
client = genai.Client()
_index: tuple[list[dict[str, str]], list[list[float]]] | None = None
What does this setup do?
- The imports provide JSON formatting, square-root calculations, file paths, and the Gemini client types.
- EMBEDDING_MODEL selects gemini-embedding-2 for document and query vectors.
- EMBEDDING_DIMENSIONS fixes every vector at 768 dimensions.
- _index starts empty so the document vectors can be cached after their first use.
- Save retriever.py.
- Confirm that retriever.py appears beside course_handbook.txt in the Visual Studio Code Explorer.
Seeing import warnings?
Confirm that Visual Studio Code still uses the .venv\Scripts\python.exe interpreter selected earlier. The Google Gen AI package is installed inside that virtual environment.
Help me check my retriever imports.
- Add the handbook parser beneath the _index declaration by copying this function:
def read_sections() -> list[dict[str, str]]:
raw_text = HANDBOOK_PATH.read_text(encoding="utf-8").strip()
sections: list[dict[str, str]] = []
for block in raw_text.split("\n\n"):
lines = [line.strip() for line in block.splitlines() if line.strip()]
if len(lines) < 2:
continue
source = lines[0].removeprefix("## ").strip()
text = " ".join(lines[1:])
sections.append({"source": source, "text": text})
if not sections:
raise RuntimeError("No handbook sections were found.")
return sections
How does the parser work?
- HANDBOOK_PATH reads the handbook stored beside retriever.py.
- The blank-line split turns each handbook section into a separate block.
- The heading becomes source. The policy paragraph becomes text.
- The runtime check stops retrieval if no usable sections were loaded.
- Save retriever.py.
- Confirm that the code from def read_sections() through return sections has no red error underlines.
Parser showing an error?
Check that every line inside read_sections() uses four spaces of indentation. Confirm that the quotation marks around \n\n remain paired.
Help me compare my parser indentation.
- Add the document embedding function beneath read_sections() by copying this code:
def embed_documents(sections: list[dict[str, str]]) -> list[list[float]]:
contents = [
types.Content(
parts=[
types.Part.from_text(
text=f"title: {section['source']} | text: {section['text']}"
)
]
)
for section in sections
]
response = client.models.embed_content(
model=EMBEDDING_MODEL,
contents=contents,
config=types.EmbedContentConfig(
output_dimensionality=EMBEDDING_DIMENSIONS
),
)
if not response.embeddings or len(response.embeddings) != len(sections):
raise RuntimeError("The handbook embedding response was incomplete.")
vectors: list[list[float]] = []
for embedding in response.embeddings:
if not embedding.values:
raise RuntimeError("An empty handbook embedding was returned.")
vectors.append(embedding.values)
return vectors
How are documents embedded?
- Each handbook entry becomes a separate types.Content value so the API returns a separate vector for every section.
- The document text follows the question-answering format with its heading supplied as the title.
- types.EmbedContentConfig requests vectors with 768 dimensions.
- The response checks prevent an incomplete vector set from entering the search index.
- Save retriever.py.
- Confirm that the closing return vectors line aligns with the first line inside embed_documents().
Embedding function showing warnings?
Check the nested parentheses around types.Content and types.Part.from_text. Confirm that the two dictionary keys use the exact names source and text.
Help me inspect my embedding function.
- Add the in-memory index function beneath embed_documents() by copying this code:
def get_index() -> tuple[list[dict[str, str]], list[list[float]]]:
global _index
if _index is None:
sections = read_sections()
_index = (sections, embed_documents(sections))
return _index
How does the cache work?
get_index() reads and embeds the handbook when _index is empty. Every later call returns the stored sections and vectors.
This avoids embedding the same eight document sections again during one running process.
- Save retriever.py.
- Confirm that return _index aligns with the global _index line.
Cache function underlined?
Check that global _index appears before the function assigns a value to _index. Confirm that read_sections() and embed_documents() match the earlier function names.
Help me fix my in-memory index function.
Rank the strongest matches
The document vectors are ready for comparison. Cosine similarity measures how closely two vectors point in the same direction.
- Add the similarity function beneath get_index() by copying this code:
def cosine_similarity(first: list[float], second: list[float]) -> float:
if len(first) != len(second):
raise ValueError("Embedding vectors must have equal lengths.")
dot_product = sum(a * b for a, b in zip(first, second))
first_magnitude = sqrt(sum(value * value for value in first))
second_magnitude = sqrt(sum(value * value for value in second))
if first_magnitude == 0 or second_magnitude == 0:
return 0.0
return dot_product / (first_magnitude * second_magnitude)
What does cosine similarity calculate?
- The length check ensures both vectors use the same number of dimensions.
- The dot product captures how the values move together.
- The two magnitudes scale the result so vector length does not dominate the comparison.
- The zero check protects the division from an empty direction.
- Save retriever.py.
- Confirm that the final calculation remains inside cosine_similarity() with four spaces of indentation.
Similarity calculation underlined?
Check that sqrt is imported from math at the top of the file. Confirm that every opening parenthesis in the magnitude calculations has a closing parenthesis.
Help me check my cosine similarity calculation.
- Add the semantic search function beneath cosine_similarity() by copying this code:
def search_handbook(query: str) -> dict[str, list[dict[str, object]]]:
sections, document_vectors = get_index()
response = client.models.embed_content(
model=EMBEDDING_MODEL,
contents=f"task: question answering | query: {query}",
config=types.EmbedContentConfig(
output_dimensionality=EMBEDDING_DIMENSIONS
),
)
if not response.embeddings or not response.embeddings[0].values:
raise RuntimeError("The query embedding response was empty.")
query_vector = response.embeddings[0].values
ranked_matches = sorted(
(
{
"source": section["source"],
"text": section["text"],
"score": round(cosine_similarity(query_vector, vector), 3),
}
for section, vector in zip(sections, document_vectors)
),
key=lambda match: float(match["score"]),
reverse=True,
)
return {"matches": ranked_matches[:2]}
How does semantic search rank policies?
- search_handbook() retrieves the cached handbook sections and document vectors.
- The question is embedded with the documented question-answering query format.
- Each document receives a rounded cosine similarity score.
- The descending sort places the closest semantic matches first.
- The returned dictionary keeps only the two strongest matches.
- Save retriever.py.
- Confirm that the ranked_matches block has no red error underlines.
Search function showing an error?
Check the indentation inside the generator expression that builds each match. Confirm that ranked_matches[:2] uses a colon between the start and end positions.
Help me inspect my search ranking code.
- Add the built-in retrieval check at the bottom of retriever.py by copying this code:
if __name__ == "__main__":
print(
json.dumps(
search_handbook("Can I submit an assessment again?"),
indent=2,
)
)
What does the retrieval check do?
The main guard runs only when you execute retriever.py directly. It searches with a paraphrased resubmission question.
json.dumps() formats the two ranked matches so their sources, policy text, and scores are easy to inspect.
- Save retriever.py.
Before you run the retriever, which handbook heading do you expect the paraphrased question to rank highest?
- Run the completed semantic retrieval check in the PowerShell terminal from earlier with this command:
.venv\Scripts\python.exe retriever.py
What does this command do?
The command runs retriever.py with the existing virtual environment interpreter. The script embeds the handbook before comparing the test question with every section.
You should see formatted JSON containing two matches. Resubmissions should appear among the top results for the paraphrased question.
That is the retrieval breakthrough. Your program can now connect different wording with the correct handbook policy.
Retriever did not return Resubmissions?
Check that course_handbook.txt contains the complete Resubmissions section. Confirm that both project files sit in the same folder.
If the command reports an authentication problem, start a new PowerShell terminal so it can read the GEMINI_API_KEY user environment variable configured earlier.
Help me diagnose my retrieval result.
✔️ Awesome, I've got everything!
Great work. Your saved retriever returns two semantic matches with source headings and similarity scores.
ⓧ I'd like to double check the full code
Compare your complete retriever.py file with this version:
import json
from math import sqrt
from pathlib import Path
from google import genai
from google.genai import types
EMBEDDING_MODEL = "gemini-embedding-2"
EMBEDDING_DIMENSIONS = 768
HANDBOOK_PATH = Path(__file__).with_name("course_handbook.txt")
client = genai.Client()
_index: tuple[list[dict[str, str]], list[list[float]]] | None = None
def read_sections() -> list[dict[str, str]]:
raw_text = HANDBOOK_PATH.read_text(encoding="utf-8").strip()
sections: list[dict[str, str]] = []
for block in raw_text.split("\n\n"):
lines = [line.strip() for line in block.splitlines() if line.strip()]
if len(lines) < 2:
continue
source = lines[0].removeprefix("## ").strip()
text = " ".join(lines[1:])
sections.append({"source": source, "text": text})
if not sections:
raise RuntimeError("No handbook sections were found.")
return sections
def embed_documents(sections: list[dict[str, str]]) -> list[list[float]]:
contents = [
types.Content(
parts=[
types.Part.from_text(
text=f"title: {section['source']} | text: {section['text']}"
)
]
)
for section in sections
]
response = client.models.embed_content(
model=EMBEDDING_MODEL,
contents=contents,
config=types.EmbedContentConfig(
output_dimensionality=EMBEDDING_DIMENSIONS
),
)
if not response.embeddings or len(response.embeddings) != len(sections):
raise RuntimeError("The handbook embedding response was incomplete.")
vectors: list[list[float]] = []
for embedding in response.embeddings:
if not embedding.values:
raise RuntimeError("An empty handbook embedding was returned.")
vectors.append(embedding.values)
return vectors
def get_index() -> tuple[list[dict[str, str]], list[list[float]]]:
global _index
if _index is None:
sections = read_sections()
_index = (sections, embed_documents(sections))
return _index
def cosine_similarity(first: list[float], second: list[float]) -> float:
if len(first) != len(second):
raise ValueError("Embedding vectors must have equal lengths.")
dot_product = sum(a * b for a, b in zip(first, second))
first_magnitude = sqrt(sum(value * value for value in first))
second_magnitude = sqrt(sum(value * value for value in second))
if first_magnitude == 0 or second_magnitude == 0:
return 0.0
return dot_product / (first_magnitude * second_magnitude)
def search_handbook(query: str) -> dict[str, list[dict[str, object]]]:
sections, document_vectors = get_index()
response = client.models.embed_content(
model=EMBEDDING_MODEL,
contents=f"task: question answering | query: {query}",
config=types.EmbedContentConfig(
output_dimensionality=EMBEDDING_DIMENSIONS
),
)
if not response.embeddings or not response.embeddings[0].values:
raise RuntimeError("The query embedding response was empty.")
query_vector = response.embeddings[0].values
ranked_matches = sorted(
(
{
"source": section["source"],
"text": section["text"],
"score": round(cosine_similarity(query_vector, vector), 3),
}
for section, vector in zip(sections, document_vectors)
),
key=lambda match: float(match["score"]),
reverse=True,
)
return {"matches": ranked_matches[:2]}
if __name__ == "__main__":
print(
json.dumps(
search_handbook("Can I submit an assessment again?"),
indent=2,
)
)
What should the full file contain?
The complete file parses the handbook and caches its document vectors. It embeds each question before ranking the two closest sections.
Your semantic retriever can now turn a paraphrased policy question into sourced handbook evidence. Next, you will let Gemini decide when to call it.
Give Gemini the Handbook Search Tool
Your semantic retrieval system can now find the handbook sections that match a learner's question. That gives your assistant a reliable source of course-policy evidence.
The retriever searches every time it receives a query. An AI agent loop uses custom function calling so Gemini can decide whether outside knowledge is needed.
In this step, get ready to:
- Describe the handbook retriever as a typed Gemini function tool.
- Inspect Gemini's response for a requested function call.
- Execute the retriever and return its results to Gemini.
Declare the handbook search tool
A function declaration tells Gemini what a local function does. Its parameter schema also tells Gemini which arguments to provide when it requests search_handbook.
- Select the course-handbook-agent folder in the file sidebar.
- Use the new-file control in the file sidebar.
- Enter agent.py as the file name.
You should see agent.py beside the existing project files.
- Add the imports and tool declaration to agent.py by copying this code:
import json
from google import genai
from retriever import search_handbook
MODEL = "gemini-3.8-flash"
client = genai.Client()
SEARCH_HANDBOOK_TOOL = {
"type": "function",
"name": "search_handbook",
"description": (
"Search the course handbook for rules about deadlines, late work, "
"attendance, grading, resubmissions, office hours, learning support, "
"or academic integrity. Use this tool for course-policy questions."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The learner's course-policy question.",
}
},
"required": ["query"],
},
}
What does this code do?
- The imports provide JSON conversion, the Gemini client, and your existing search_handbook function.
- The MODEL value selects the Gemini model used by the agent.
- The SEARCH_HANDBOOK_TOOL dictionary describes when Gemini should request a handbook search.
- The query schema requires Gemini to supply the learner's policy question as text.
- Save agent.py.
Your editor should show the saved tool declaration without red syntax markers.
Seeing a syntax marker?
Check that every opening brace has a matching closing brace. Confirm that SEARCH_HANDBOOK_TOOL starts at the left edge of the file.
Ask for help with the declaration.
The tool description controls when Gemini considers the retriever relevant. The system instruction sets the broader rule that course facts must come from handbook evidence.
- Add the system instruction and initial interaction below SEARCH_HANDBOOK_TOOL by copying this code:
SYSTEM_INSTRUCTION = (
"You are a course assistant. For any question about course rules, dates, "
"grading, attendance, deadlines, support, or resubmissions, call "
"search_handbook and answer only from its results. Do not guess handbook "
"facts. For unrelated general questions, answer directly without calling "
"the tool. When a tool is used, mention the handbook section names."
)
def ask_agent(question: str) -> dict[str, object]:
interaction = client.interactions.create(
model=MODEL,
input=question,
system_instruction=SYSTEM_INSTRUCTION,
tools=[SEARCH_HANDBOOK_TOOL],
)
How does Gemini receive the tool?
The SYSTEM_INSTRUCTION separates course-policy questions from general questions. It also prevents unsupported policy answers.
The tools argument makes the declaration available during the interaction. Gemini can now request the function when the question matches its purpose.
- Save agent.py.
Your editor should keep the new ask_agent() function indented beneath its definition.
Is the function indentation broken?
Keep the interaction assignment four spaces inside ask_agent(). Place SYSTEM_INSTRUCTION at the left edge of the file.
Ask for help with the function structure.
Inspect Gemini's tool decision
A Gemini interaction records its reasoning actions in interaction.steps. A requested tool appears as a step whose type is function_call.
- Add the tool-call inspection directly below the interaction in ask_agent() by copying this code:
tool_call = next(
(
step
for step in (interaction.steps or [])
if step.type == "function_call"
),
None,
)
if tool_call is None:
return {
"answer": interaction.output_text or "No answer was returned.",
"tool_used": False,
"sources": [],
}
How is the decision detected?
The next() expression searches the interaction steps for the first function request. It returns None when Gemini answers directly.
The direct-answer branch reports tool_used as False. Its empty sources list makes the bypass visible.
- Save agent.py.
Your editor should show tool_call inside ask_agent() without red indentation markers.
Seeing an indentation marker?
Keep tool_call aligned with interaction. Keep the direct return nested beneath if tool_call is None:.
Ask for help with this branch.
- Add the two test questions at the bottom of agent.py by copying this code:
if __name__ == "__main__":
print(ask_agent("Can I submit an assessment again?"))
print(ask_agent("Say hello in one sentence."))
Why test two questions?
The first question requires a fictional handbook rule. The second question can be answered from general language ability.
Running both questions reveals whether Gemini makes different tool decisions for different goals.
- Save agent.py.
Before you run the partial agent, which question do you think reaches the unfinished tool-call path?
- Run the partial agent from the existing PowerShell terminal with this command:
.venv\Scripts\python.exe agent.py
What should you see at this checkpoint?
The policy question prints None because Gemini requested the tool. Your function detects that request without executing it yet.
The greeting returns an answer with tool_used set to False. Its sources list is empty.
This partial result is intentional. You have proved that Gemini can distinguish a policy question from a general greeting.
Did both questions return None?
Check that the direct-answer dictionary is nested beneath if tool_call is None:. A misplaced return can prevent the greeting from reaching that branch.
Ask for help with the decision check.
Detecting a function request completes only the model's side of the loop. Your Python code must validate that request before executing the local retriever.
- Find the if __name__ == "__main__": line near the bottom of agent.py.
- Insert the tool execution block directly above that line by copying this code:
if tool_call.name != "search_handbook":
raise RuntimeError(f"Unexpected tool requested: {tool_call.name}")
if not interaction.id or not tool_call.id:
raise RuntimeError("The function call did not include required IDs.")
result = search_handbook(**tool_call.arguments)
sources = [str(match["source"]) for match in result["matches"]]
final_interaction = client.interactions.create(
model=MODEL,
previous_interaction_id=interaction.id,
system_instruction=SYSTEM_INSTRUCTION,
tools=[SEARCH_HANDBOOK_TOOL],
input=[
{
"type": "function_result",
"name": tool_call.name,
"call_id": tool_call.id,
"result": [
{
"type": "text",
"text": json.dumps(result),
}
],
}
],
)
How does the tool loop continue?
- The name check prevents the agent from executing an unexpected function.
- The ID check confirms that the request can be matched to its result.
- The local search_handbook() function returns two ranked handbook matches.
- The second interaction sends a function_result through previous_interaction_id.
- Save agent.py.
Your editor should show the execution block inside ask_agent(). The bottom test block should remain at the left edge.
Is the bottom test block indented?
Keep every line through final_interaction four spaces inside ask_agent(). Move if __name__ == "__main__": back to the left edge.
Ask for help with the insertion point.
- Add the tool-result return directly below the closing parenthesis of final_interaction by copying this code:
return {
"answer": final_interaction.output_text or "No answer was returned.",
"tool_used": True,
"sources": sources,
}
What does the final return expose?
The returned dictionary contains Gemini's evidence-based answer. It also reports that the tool was used.
The sources list preserves the handbook headings behind the answer. That makes the agent's evidence visible.
- Save agent.py.
Test both tool decisions
Your agent now has both paths. A policy question can complete the function loop while a greeting can bypass it.
Before you run the finished agent, what differences do you expect between the two result dictionaries?
- Run the finished agent from the existing PowerShell terminal with this command:
.venv\Scripts\python.exe agent.py
What should you see?
The policy result should show tool_used as True. Its sources list should include Resubmissions.
The greeting result should show tool_used as False. Its sources list should be empty.
That is the full agent loop working. Gemini now chooses the handbook only when the learner's question requires it.
Is the policy question missing sources?
Confirm that result comes from search_handbook(**tool_call.arguments). Check that sources reads each match's source value.
If the API request fails, confirm that the existing GEMINI_API_KEY user environment variable is available in this PowerShell session.
Ask for help with the completed loop.
✔️ Awesome, I've got everything!
Great. Double check that agent.py is saved. Confirm that the two questions produce different tool decisions.
ⓧ I'd like to double check the full code
Compare your complete agent.py file with this version:
import json
from google import genai
from retriever import search_handbook
MODEL = "gemini-3.8-flash"
client = genai.Client()
SEARCH_HANDBOOK_TOOL = {
"type": "function",
"name": "search_handbook",
"description": (
"Search the course handbook for rules about deadlines, late work, "
"attendance, grading, resubmissions, office hours, learning support, "
"or academic integrity. Use this tool for course-policy questions."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The learner's course-policy question.",
}
},
"required": ["query"],
},
}
SYSTEM_INSTRUCTION = (
"You are a course assistant. For any question about course rules, dates, "
"grading, attendance, deadlines, support, or resubmissions, call "
"search_handbook and answer only from its results. Do not guess handbook "
"facts. For unrelated general questions, answer directly without calling "
"the tool. When a tool is used, mention the handbook section names."
)
def ask_agent(question: str) -> dict[str, object]:
interaction = client.interactions.create(
model=MODEL,
input=question,
system_instruction=SYSTEM_INSTRUCTION,
tools=[SEARCH_HANDBOOK_TOOL],
)
tool_call = next(
(
step
for step in (interaction.steps or [])
if step.type == "function_call"
),
None,
)
if tool_call is None:
return {
"answer": interaction.output_text or "No answer was returned.",
"tool_used": False,
"sources": [],
}
if tool_call.name != "search_handbook":
raise RuntimeError(f"Unexpected tool requested: {tool_call.name}")
if not interaction.id or not tool_call.id:
raise RuntimeError("The function call did not include required IDs.")
result = search_handbook(**tool_call.arguments)
sources = [str(match["source"]) for match in result["matches"]]
final_interaction = client.interactions.create(
model=MODEL,
previous_interaction_id=interaction.id,
system_instruction=SYSTEM_INSTRUCTION,
tools=[SEARCH_HANDBOOK_TOOL],
input=[
{
"type": "function_result",
"name": tool_call.name,
"call_id": tool_call.id,
"result": [
{
"type": "text",
"text": json.dumps(result),
}
],
}
],
)
return {
"answer": final_interaction.output_text or "No answer was returned.",
"tool_used": True,
"sources": sources,
}
if __name__ == "__main__":
print(ask_agent("Can I submit an assessment again?"))
print(ask_agent("Say hello in one sentence."))
Your agent can now choose its handbook tool and expose the evidence behind each policy answer. Next, you will place this workflow behind a validated FastAPI endpoint.
Expose the Agent Through FastAPI
Your agent already chooses when to search the handbook through Gemini. Right now, that behavior is available only through a terminal script.
An HTTP boundary makes the agent easier to demonstrate. FastAPI adds that boundary without requiring a separate frontend.
In this step, get ready to:
- Create a FastAPI application with typed request and response models.
- Connect the POST /ask route to ask_agent.
- Test policy and general questions through the interactive documentation.
Create the FastAPI application
A FastAPI application describes your service before any routes are added. Its title appears in the interactive documentation.
- Use the Explorer sidebar in VS Code to create main.py inside the course-handbook-agent folder.
- Add the application imports and metadata to main.py by pasting this code:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from agent import ask_agent
app = FastAPI(
title="Course Handbook Agent",
description="A Gemini agent that decides when to search a local handbook.",
version="1.0.0",
)
What does this code do?
- The imports bring in the API framework and its error type.
- The BaseModel import prepares the file for typed API data.
- The ask_agent import gives the service access to the completed agent loop.
- The app object stores the service metadata shown in the documentation.
- Save main.py.
Uvicorn keeps the terminal busy while the service runs. You can stop it later with Ctrl+C.
- Start the local service from the existing terminal panel by running this command:
.venv\Scripts\uvicorn.exe main:app --reload --port 5000
What does this command do?
The command starts the app object from main.py on port 5000. The --reload option reloads the service whenever you save a Python file.
- Keep the Uvicorn terminal running for the rest of this step.
- Open http://127.0.0.1:5000/docs in your browser.
You'll see interactive documentation titled Course Handbook Agent. Your local API foundation is live.
Can't load the interactive docs?
Check the Uvicorn terminal for a traceback. Confirm that main.py is inside the course-handbook-agent folder.
Confirm that the terminal is running from the same folder. Keep the server process active while you load the documentation.
Help me troubleshoot why my local FastAPI documentation is not loading.
Define the API contract
Pydantic models define the data accepted and returned by your endpoint. This contract makes the question format and source metadata visible before a request runs.
- Place these models below the app definition in main.py by pasting this code:
class AskRequest(BaseModel):
question: str
class AskResponse(BaseModel):
answer: str
tool_used: bool
sources: list[str]
What do these models define?
- AskRequest requires each incoming request to contain a string named question.
- AskResponse guarantees that each response includes an answer.
- The tool_used field exposes the agent's tool decision.
- The sources field exposes any handbook section headings used by the answer.
- Save main.py.
- Switch to the terminal running Uvicorn.
You'll see Uvicorn reload main.py without a traceback. The typed contract now loads successfully.
Seeing a model error after reload?
Compare the indentation beneath AskRequest and AskResponse. Each field must sit inside its class.
Help me fix the Pydantic models in main.py.
A small root route gives you a quick health check outside the interactive documentation. It also points clients toward /docs.
- Place the root route below AskResponse by pasting this code:
@app.get("/")
def read_root() -> dict[str, str]:
return {
"message": "Course Handbook Agent is running.",
"docs": "/docs",
}
What does this route do?
The read_root function handles requests to /. Its response confirms that the service is running and provides the documentation path.
- Save main.py.
- Open http://127.0.0.1:5000 in your browser.
You'll see a JSON response containing Course Handbook Agent is running. and /docs. This proves the service can answer an HTTP request.
Root route missing?
Confirm that @app.get("/") appears below the model classes. Save main.py before refreshing the browser.
Help me debug the missing FastAPI root route.
Connect POST /ask to the agent
The final route turns an incoming question into an agent call. It returns the typed result or converts an unexpected failure into an HTTP error response.
- Place the question route below read_root by pasting this code:
@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest) -> AskResponse:
try:
result = ask_agent(request.question)
return AskResponse(**result)
except Exception as error:
raise HTTPException(status_code=500, detail=str(error)) from error
How does the route connect to the agent?
- The route accepts data shaped like AskRequest.
- The ask_agent call sends the submitted question through the existing agent loop.
- AskResponse validates the answer and its metadata before returning them.
- HTTPException converts an unexpected failure into a response with status code 500.
- Save main.py.
- Switch to the terminal running Uvicorn.
You'll see another reload without a traceback. The service now exposes the complete agent workflow.
✔️ Awesome, I've got everything!
Great. Confirm that main.py is saved and that Uvicorn completed its reload.
ⓧ I'd like to double check the full code
Compare your complete main.py file with this reference:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from agent import ask_agent
app = FastAPI(
title="Course Handbook Agent",
description="A Gemini agent that decides when to search a local handbook.",
version="1.0.0",
)
class AskRequest(BaseModel):
question: str
class AskResponse(BaseModel):
answer: str
tool_used: bool
sources: list[str]
@app.get("/")
def read_root() -> dict[str, str]:
return {
"message": "Course Handbook Agent is running.",
"docs": "/docs",
}
@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest) -> AskResponse:
try:
result = ask_agent(request.question)
return AskResponse(**result)
except Exception as error:
raise HTTPException(status_code=500, detail=str(error)) from error
What should the complete file contain?
This file combines the FastAPI application metadata with two Pydantic models. It also exposes the health-check route and the agent route.
Before you send these requests, which question do you expect to trigger the handbook search?
- Return to http://127.0.0.1:5000/docs in your browser.
- Expand POST /ask in the interactive documentation.
- Enable editing for the request body.
- Set the question field to Can I submit an assessment again?.
- Submit the request.
You'll see an answer based on the fictional resubmission policy. The response shows tool_used as true with Resubmissions in sources.
- Set the question field to Say hello in one sentence.
- Submit the request.
You'll see a short greeting with tool_used set to false. The sources list is empty.
Request returning an error?
Check the Uvicorn terminal for the underlying Python traceback. Confirm that the existing agent.py file still runs successfully.
Confirm that the request contains a string field named question. Keep the GEMINI_API_KEY user environment variable available to the Uvicorn process.
Help me diagnose an error from POST /ask.
You made the agent accessible through a typed local API. Its responses now show both the tool decision and the handbook sources through a browser interface.
Secret mission
Expand the Agent's Knowledge Base
Add a fictional equipment-loan policy to the handbook. Then prove that direct and paraphrased questions retrieve it without changing any Python code.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep these free local resources, pause the server, or remove everything. Gemini API requests use the standard free tier.
Keep the Handbook Fictional
Prompts sent through the free tier may be used to improve Google's products.
- Keep confidential information out of course_handbook.txt.
- Continue using fictional policies when you extend the handbook.
Resources you used:
- A Google AI Studio authorization API key.
- The GEMINI_API_KEY Windows user environment variable.
- The course-handbook-agent folder.
- A local Uvicorn process serving the FastAPI application.
Keep everything running
No action is needed. Choose this if you plan to keep demonstrating or extending the course handbook agent.
- Keep the GEMINI_API_KEY user variable private.
- Continue using fictional content in course_handbook.txt.
- Leave the Uvicorn terminal running during demonstrations.
- Stop Uvicorn with Ctrl+C after each demonstration.
Pause - I'll come back to this later
Pausing returns control of your terminal without removing the project. Your scripts and virtual environment remain available for later.
- Switch back to the terminal running Uvicorn from earlier.
- Press Ctrl+C to stop the server.
- Confirm the PowerShell prompt returns.
- On a shared computer, return to the Environment Variables window from earlier to remove GEMINI_API_KEY from User variables.
- Keep the Google AI Studio authorization key active for your next session.
Delete - I don't want to use this again
Remove the local project resources to start fresh. This cleanup permanently deletes your local files while leaving your Google account intact.
- Copy any project code you want to keep.
- Stop Uvicorn by pressing Ctrl+C in its terminal if it is still running.
- Return to the API key management page in Google AI Studio to identify the authorization API key created for this project.
- Revoke the authorization API key created for this project.
- Return to the Environment Variables window from earlier to find GEMINI_API_KEY under User variables.
- Select GEMINI_API_KEY.
- Click Delete to remove the selected user variable.
That takes care of the sensitive part. Your local scripts no longer have a saved Gemini credential.
Delete Local Files
- In the Explorer sidebar in Visual Studio Code, right-click the course-handbook-agent folder.
- Choose the option that reveals the folder in File Explorer.
- Close Visual Studio Code.
- Move to the parent folder that contains course-handbook-agent.
- Select the course-handbook-agent folder.
- Delete the selected folder.
- Confirm course-handbook-agent no longer appears in File Explorer.
Nice Work!
Nice Work!
You did it! Your local course handbook agent now uses Gemini to choose when handbook evidence is needed.
What you learned:
- Tested a direct Gemini interaction against a fictional course-policy question. Revealed the model's knowledge boundary when handbook evidence is unavailable.
- Built a semantic retriever with Gemini Embedding 2. Ranked paraphrased questions using cosine similarity. Returned source headings with each match.
- Implemented an AI agent loop that lets Gemini request search_handbook for policy questions. Returned the function result for a sourced final answer. Exposed the workflow through a validated FastAPI endpoint. Made every tool decision inspectable through tool_used and sources.
- Completed the Secret Mission by extending course_handbook.txt with an Equipment Loans policy without changing Python code. Confirmed an exact policy question retrieves the new section. Confirmed a paraphrased policy question does the same. Kept unrelated greetings on the direct-answer path.
Ready to quiz yourself?