Build a Gemini Knowledge Assistant
Build a cited knowledge assistant with Node.js or Python.
Introduction
30 Second Summary
Important details can get buried across notes long before you need them. Finding one fact often means reopening documents until the right sentence appears.
In this project, you will build a command-line assistant that turns fictional Project Atlas notes into a searchable knowledge base. You will use retrieval-augmented generation through Gemini File Search to answer private questions with source citations.
What You'll Build
You will ask about Project Atlas and watch the assistant return the exact budget and deadline with a citation to the uploaded note.
By the end of this project, you'll have:
- An ungrounded baseline that exposes what a general model cannot reliably know about your private facts.
- A searchable note collection that finds relevant Project Atlas details through semantic search.
- Evidence-controlled answers that show citations for supported facts or decline questions the documents cannot answer.
- Secret Mission: Add a second knowledge category and restrict retrieval with metadata filtering.
Are there any prerequisites?
You need a Windows computer with Visual Studio Code and Node.js 20 or later installed.
You also need a Google account for the Gemini API key setup in Google AI Studio.
Before We Start
Before the hands-on work begins, you need a clear purpose for your personal knowledge assistant. Choosing a non-sensitive document collection and a useful question gives the retrieval workflow a concrete goal.
Set Up the Gemini Node.js Project
Your assistant needs a secure path to the Gemini API before it can search any notes.
A compatible Node.js runtime executes the project. The pinned Google Gen AI SDK gives your code a stable server-side client.
In this step, get ready to:
- Create the personal-knowledge-assistant workspace in Visual Studio Code.
- Install @google/genai at version 2.21.0 with npm.
- Protect a Gemini API key in a Windows user environment variable.
Create the project and install the SDK
A dedicated workspace keeps the assistant files together. It also gives the integrated PowerShell terminal the correct project location.
- Press the Windows key to open search.
- Type Visual Studio Code and press Enter.
- Select File from the top menu.
- Select Open Folder....
- Select your Desktop.
- Select New Folder.
- Name the folder personal-knowledge-assistant and press Enter.
- Select Select Folder.
What is Workspace Trust?
Workspace Trust controls whether code inside an opened folder can run. You created this empty folder yourself, so you can trust its contents.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
You should see personal-knowledge-assistant at the top of the Explorer view.
The SDK requires Node.js 20 or later. Check the installed runtime before adding project files.
- Select View from the VS Code menu.
- Select Terminal to open the integrated terminal.
- Check your installed Node.js version by running this command:
node --version
What does this command show?
The command prints the Node.js version used by this PowerShell terminal. The major version is the first number in the output.
Which result matches your terminal?
✔️ I see a supported version
Your Node.js runtime meets the 20 or later requirement.
ⓧ I see an older version
The installed runtime is below the SDK requirement. The official Windows installer provides the current Node.js LTS release.
- Download Node.js v24.21.0 LTS from the official Node.js download page.
- Run the downloaded installer.
- Keep the default installation choices.
- Close the current PowerShell terminal after installation.
- Select View from the VS Code menu.
- Select Terminal to open a new terminal.
- Repeat the version check shown above.
- Confirm the first number is 20 or higher.
ⓧ Command not found
PowerShell cannot find a Node.js installation. Install the current LTS release before continuing.
- Download Node.js v24.21.0 LTS from the official Node.js download page.
- Run the downloaded installer.
- Keep the default installation choices.
- Close the current PowerShell terminal after installation.
- Select View from the VS Code menu.
- Select Terminal to open a new terminal.
- Repeat the version check shown above.
- Confirm PowerShell now prints a version whose first number is 20 or higher.
The project manifest pins the SDK version and tells Node.js to use JavaScript modules. It also records the minimum runtime requirement.
- Select the Explorer view in the left Activity Bar.
- Select New File... in the Explorer view.
- Name the file package.json and press Enter.
- Define the project metadata and pinned dependency by pasting this code into package.json:
{
"name": "personal-knowledge-assistant",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@google/genai": "2.21.0"
}
}
What does this file define?
- The project name identifies this local Node.js package.
- The module setting enables the import syntax used by the assistant.
- The engine requirement records Node.js 20.0.0 as the minimum version.
- The dependency entry pins @google/genai to exactly 2.21.0.
- Press Ctrl+S to save package.json.
- Confirm package.json appears beneath personal-knowledge-assistant in the Explorer view.
Seeing a problem in package.json?
- Check that every property except the final one in each object ends with a comma.
- Check that every property name and text value uses double quotes.
Ask for help with checking the package.json syntax.
The ignore file keeps installed packages and the future store record out of Git. This prevents generated project state from entering source control.
- Select New File... in the Explorer view.
- Name the file .gitignore and press Enter.
- Define the ignored project state by pasting this code into .gitignore:
node_modules/
.knowledge-store.json
Why ignore these files?
The node_modules/ directory contains packages that npm can restore. The future .knowledge-store.json file records the cloud store created later.
- Press Ctrl+S to save .gitignore.
- Confirm .gitignore appears in the Explorer view.
- Install the dependency declared in package.json by running this command:
npm install
What does npm install do?
npm reads package.json and installs the pinned SDK into node_modules. The ignore rule keeps that generated directory out of Git.
- Wait for the installation command to finish.
- Confirm node_modules appears in the Explorer view.
- Confirm the terminal finishes without an installation error.
Your pinned SDK is now installed inside the project.
Did the dependency installation fail?
- Confirm the terminal path ends with personal-knowledge-assistant.
- Confirm package.json matches the code above.
- Confirm the Node.js version check reports 20 or higher.
Ask for help with diagnosing the npm installation.
Store the Gemini API key in Windows
A Gemini API key authenticates requests from your local assistant. Google AI Studio may ask you to accept its terms or choose a Google Cloud project before creating the key.
This key works like a password. Keep it off screen whenever you share your setup.
- Open Google AI Studio in your browser.
- Sign in with your Google account if prompted.
- Accept the terms if prompted.
- Open Dashboard from the left panel.
- Select API Keys.
- Click Create API key.
- Choose your Google Cloud project if prompted.
- Copy the new API key.
Keep the key out of Git
Store the key only in the Windows user environment. Never paste it into package.json, check-env.js, or any other project file.
- Press the Windows key to open search.
- Type Environment Variables.
- Open Environment Variables in System Properties.
- Create a new variable under the user variables section.
- Set the variable name to GEMINI_API_KEY.
- Paste the copied API key as the variable value.
- Save the new user variable.
Your credential is now separated from the project files, which protects it from accidental commits.
- Close the existing PowerShell terminal in VS Code.
- Select View from the VS Code menu.
- Select Terminal to open a fresh PowerShell terminal.
The new terminal inherits the saved GEMINI_API_KEY user variable without exposing its value.
Validate the complete environment
The environment check tests the runtime before creating an SDK client. A single success message confirms that the project can safely move on to its first API request.
- Select New File... in the Explorer view.
- Name the file check-env.js and press Enter.
- Build the environment check by pasting this code into check-env.js:
import { GoogleGenAI } from "@google/genai";
const nodeMajor = Number(process.versions.node.split(".")[0]);
if (nodeMajor < 20) {
throw new Error("Node.js 20 or later is required.");
}
if (!process.env.GEMINI_API_KEY) {
throw new Error("GEMINI_API_KEY is not available in this terminal.");
}
new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
console.log(
`Ready: Node.js ${process.versions.node}, @google/genai loaded, and GEMINI_API_KEY is available.`,
);
What does this code check?
- The version check reads the active Node.js major version and stops below 20.
- The environment check confirms that GEMINI_API_KEY is available to this terminal.
- The client creation proves that the installed SDK can be imported.
- The final log reports that all three checks passed.
- Press Ctrl+S to save check-env.js.
- Confirm check-env.js appears in the Explorer view.
Before you run the check, which of the three requirements do you expect it to confirm?
- Validate the Node.js version, SDK import, and environment variable by running:
node check-env.js
What does this check prove?
The command executes check-env.js in the current PowerShell terminal. Reaching the final log means every prerequisite passed.
You should see a line beginning with Ready: Node.js and ending with @google/genai loaded, and GEMINI_API_KEY is available.
That is the foundation locked in: your runtime, pinned SDK, and protected credential now work together.
Did the environment check stop?
- Follow the older-version tab above if the Node.js requirement fails.
- Confirm npm install completed if the SDK import fails.
- Confirm the user variable is named exactly GEMINI_API_KEY if the credential check fails. Open a fresh terminal after correcting it.
Ask for help with diagnosing the environment check.
Use the tabs below to compare your saved setup files with the complete project state.
✔️ Awesome, I've got everything!
Your three setup files are saved inside personal-knowledge-assistant.
ⓧ I'd like to double check the full code
- Compare each saved file with its complete version below.
{
"name": "personal-knowledge-assistant",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@google/genai": "2.21.0"
}
}
What should package.json contain?
This complete file pins the SDK dependency and records the required Node.js runtime.
node_modules/
.knowledge-store.json
What should .gitignore contain?
This complete file excludes installed packages and the future local store record.
import { GoogleGenAI } from "@google/genai";
const nodeMajor = Number(process.versions.node.split(".")[0]);
if (nodeMajor < 20) {
throw new Error("Node.js 20 or later is required.");
}
if (!process.env.GEMINI_API_KEY) {
throw new Error("GEMINI_API_KEY is not available in this terminal.");
}
new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
console.log(
`Ready: Node.js ${process.versions.node}, @google/genai loaded, and GEMINI_API_KEY is available.`,
);
What should check-env.js contain?
This complete file verifies the runtime, imports the SDK, checks the environment variable, and prints the success message.
Your secure Gemini project foundation is ready. Next, you will ask the model a private Project Atlas question without a knowledge base and see the retrieval problem firsthand.
Ask Without Your Knowledge Base
Your Node.js project is ready to make its first request to the Gemini API. This step tests what the model knows before it receives any private notes.
The fictional Project Atlas budget plus pilot deadline have never been supplied to the general model. A plain request makes that knowledge gap visible.
In this step, get ready to:
- Create baseline.js with a plain Gemini Interactions API request.
- Ask about Project Atlas without supplying a source document.
- Check whether the answer is supported by a document citation.
Build the ungrounded request
The baseline uses only your question as input. It prints the model's answer without processing any document sources.
- In the VS Code Explorer sidebar, create baseline.js inside personal-knowledge-assistant by selecting the file icon with a plus sign.
- Add the baseline request to baseline.js by pasting this code:
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const question =
process.argv.slice(2).join(" ").trim() ||
"What are Project Atlas's budget ceiling and pilot deadline?";
const interaction = await ai.interactions.create({
model: "gemini-3.8-flash",
input: question,
});
console.log("\nUngrounded answer:\n");
console.log(interaction.output_text);
What does this code do?
- The GoogleGenAI client reads the API key from the environment variable. Your credential stays outside the source file.
- The question value combines the words passed after the script name. It falls back to the Project Atlas question when no words are supplied.
- The ai.interactions.create() call sends the question to gemini-3.8-flash.
- The interaction.output_text value contains the model's response for the terminal.
- Save baseline.js.
- Confirm that baseline.js appears beside package.json in the Explorer sidebar.
Can't find the new file?
Check that baseline.js appears inside personal-knowledge-assistant. A file saved in another folder is outside this project.
Ask for help with the file location using this troubleshooting prompt.
✔️ Awesome, I've got everything!
Your baseline.js file now matches the project baseline. Keep it saved before testing.
ⓧ I'd like to double check the full code
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const question =
process.argv.slice(2).join(" ").trim() ||
"What are Project Atlas's budget ceiling and pilot deadline?";
const interaction = await ai.interactions.create({
model: "gemini-3.8-flash",
input: question,
});
console.log("\nUngrounded answer:\n");
console.log(interaction.output_text);
What should match?
- The SDK import appears first.
- The command-line question includes its default Project Atlas prompt.
- The interaction uses gemini-3.8-flash.
- The final statements print the ungrounded answer.
Test the private question
The script is deliberately limited to the question. Running it creates the visible shortfall that the rest of your assistant needs to address.
- Return to the VS Code PowerShell terminal from earlier.
Before you run this, form your prediction about whether the model can reliably give both private values from the question alone.
- Test the ungrounded request by running:
node baseline.js "What are Project Atlas's budget ceiling and pilot deadline?"
What should I see?
- The terminal prints Ungrounded answer: followed by the model's response.
- The response may admit that context is missing.
- The response may offer unsupported generalities.
- You receive no document citation because this request included no document.
- Inspect the response for any claim about the budget ceiling.
- Inspect the response for any claim about the pilot deadline.
- Confirm that no document citation appears below the answer.
That shortfall is useful. Your baseline now proves the model cannot ground these private facts in evidence.
Your baseline has exposed the knowledge gap. Next, you will turn the Project Atlas note into a searchable source of truth.
Index Your Personal Notes
Your baseline call proved that Gemini has no access to the private Project Atlas facts. The assistant now needs a searchable source of truth for those details.
A Gemini File Search store turns an uploaded note into retrievable embeddings. Semantic search can then match a question with relevant passages based on meaning.
In this step, get ready to:
- Create a fictional Project Atlas note with private source facts.
- Build a bounded indexing workflow for a persistent File Search store.
- Run the workflow to save the store resource name locally.
Create the Project Atlas note
The uploaded source is a fictional Markdown note. Its headings separate the private facts that the assistant needs to retrieve later.
- Create a new file at knowledge/atlas.md inside the personal-knowledge-assistant folder using the VS Code file sidebar.
- Populate knowledge/atlas.md by pasting the content below:
# Project Atlas
Project Atlas is an internal searchable playbook for the product and support teams.
## Ownership
The decision owner is Maya Chen. Weekly status updates are sent every Thursday.
## Pilot
The pilot deadline is October 18, 2026. The pilot group contains 25 employees from product, support, and operations.
## Budget
The approved budget ceiling is $4,800. Any increase requires written approval from Maya Chen.
## Main risk
The largest known risk is poor OCR quality in legacy PDF documents. The team will manually review the ten most-used legacy files before the pilot.
## Success criterion
The pilot succeeds if at least 80 percent of the evaluation questions receive a correct answer with a usable source citation.
What does this note contain?
- The ownership section identifies the decision owner for Project Atlas.
- The pilot section records the deadline and pilot group.
- The budget section provides the approved spending ceiling.
- The final sections capture the main risk and measurable success criterion.
- Save knowledge/atlas.md.
- Confirm that the note contains headings for ownership, pilot, budget, main risk, and success criterion.
Good progress. Your sample note now gives the retrieval workflow a clear set of facts to index.
Does the note look incomplete?
Check that atlas.md sits inside the knowledge folder. Confirm that each section heading begins with ##.
Compare the dates and numeric values carefully because the later questions depend on those exact facts.
Help me compare my Project Atlas note with the required content.
✔️ Awesome, I've got everything!
Your Project Atlas source note is complete and saved.
ⓧ I'd like to double check the full code
# Project Atlas
Project Atlas is an internal searchable playbook for the product and support teams.
## Ownership
The decision owner is Maya Chen. Weekly status updates are sent every Thursday.
## Pilot
The pilot deadline is October 18, 2026. The pilot group contains 25 employees from product, support, and operations.
## Budget
The approved budget ceiling is $4,800. Any increase requires written approval from Maya Chen.
## Main risk
The largest known risk is poor OCR quality in legacy PDF documents. The team will manually review the ten most-used legacy files before the pilot.
## Success criterion
The pilot succeeds if at least 80 percent of the evaluation questions receive a correct answer with a usable source citation.
Build the indexing workflow
Indexing needs a persistent File Search store and a bounded wait for the upload operation. The script also keeps the returned store name so later commands can search the same data.
- Create ingest.js inside the personal-knowledge-assistant folder using the VS Code file sidebar.
- Add the SDK setup, polling helper, and store configuration by pasting the first code chunk below:
import { writeFileSync } from "node:fs";
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const STORE_FILE = ".knowledge-store.json";
async function waitForIndexing(operation) {
for (let attempt = 0; attempt < 24; attempt += 1) {
if (operation.done || operation.response?.documentName) {
return operation;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
operation = await ai.operations.get({ operation });
}
throw new Error(
"Indexing did not report completion within two minutes. Run cleanup.js before retrying.",
);
}
const store = await ai.fileSearchStores.create({
config: {
displayName: "personal-knowledge-assistant",
embeddingModel: "models/gemini-embedding-2",
},
});
What does this foundation do?
- The SDK client authenticates with the GEMINI_API_KEY available in your terminal.
- The waitForIndexing() helper checks the upload operation at five-second intervals.
- The loop stops after 24 attempts to prevent an unlimited wait.
- The store configuration names the resource personal-knowledge-assistant and selects models/gemini-embedding-2 for indexing.
- Save ingest.js.
- Confirm that waitForIndexing() contains a maximum of 24 polling attempts.
Seeing editor warnings?
Check that both import lines sit at the top of ingest.js. Match every opening brace with its closing brace.
Confirm that models/gemini-embedding-2 appears inside the store configuration.
Help me find a syntax problem in this part of ingest.js.
The store exists in the cloud after creation. The remaining logic uploads the note and saves the resource name only after indexing succeeds.
- Place your cursor below the store creation block in ingest.js.
- Complete the indexing workflow by pasting this code below it:
if (!store.name) {
throw new Error("Gemini did not return a File Search store name.");
}
console.log(`Created ${store.name}`);
try {
let operation = await ai.fileSearchStores.uploadToFileSearchStore({
file: "knowledge/atlas.md",
fileSearchStoreName: store.name,
config: {
displayName: "Project Atlas notes",
},
});
operation = await waitForIndexing(operation);
writeFileSync(STORE_FILE, JSON.stringify({ name: store.name }, null, 2));
console.log("Indexed knowledge/atlas.md and saved .knowledge-store.json.");
} catch (error) {
await ai.fileSearchStores.delete({
name: store.name,
config: { force: true },
});
throw error;
}
How does the upload stay safe?
- The name check stops the script if Gemini does not return a store resource name.
- The upload gives knowledge/atlas.md the display name Project Atlas notes.
- The script writes .knowledge-store.json only after the indexing operation completes.
- The catch block deletes the new store if the upload or indexing operation fails.
- Save ingest.js.
- Confirm that writeFileSync appears after the call to waitForIndexing().
Does the script structure look wrong?
Check that the second chunk begins after the closing }); from store creation. Keep the upload and local file write inside the try block.
Make sure the deletion call remains inside the catch block so a failed attempt does not leave a partial store.
Help me check the upload and cleanup structure in ingest.js.
✔️ Awesome, I've got everything!
Your complete indexing script is saved and ready to run.
ⓧ I'd like to double check the full code
import { writeFileSync } from "node:fs";
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const STORE_FILE = ".knowledge-store.json";
async function waitForIndexing(operation) {
for (let attempt = 0; attempt < 24; attempt += 1) {
if (operation.done || operation.response?.documentName) {
return operation;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
operation = await ai.operations.get({ operation });
}
throw new Error(
"Indexing did not report completion within two minutes. Run cleanup.js before retrying.",
);
}
const store = await ai.fileSearchStores.create({
config: {
displayName: "personal-knowledge-assistant",
embeddingModel: "models/gemini-embedding-2",
},
});
if (!store.name) {
throw new Error("Gemini did not return a File Search store name.");
}
console.log(`Created ${store.name}`);
try {
let operation = await ai.fileSearchStores.uploadToFileSearchStore({
file: "knowledge/atlas.md",
fileSearchStoreName: store.name,
config: {
displayName: "Project Atlas notes",
},
});
operation = await waitForIndexing(operation);
writeFileSync(STORE_FILE, JSON.stringify({ name: store.name }, null, 2));
console.log("Indexed knowledge/atlas.md and saved .knowledge-store.json.");
} catch (error) {
await ai.fileSearchStores.delete({
name: store.name,
config: { force: true },
});
throw error;
}
Run the indexing workflow
The first run creates a persistent cloud resource and converts the note into searchable representations. The saved resource name connects later questions to this exact store.
Indexing can take up to two minutes while Gemini processes the note. A quiet pause during the polling loop is expected.
Before you run this, what two messages do you expect the script to print?
- Index the Project Atlas note by running this command in the VS Code PowerShell terminal:
node ingest.js
You will first see a resource name beginning with fileSearchStores/. You will then see Indexed knowledge/atlas.md and saved .knowledge-store.json. when indexing completes.
That is the retrieval foundation in place. Your File Search store now contains the Project Atlas note and has a local record for later requests.
Why save the store name?
The File Search store remains available until it is manually deleted. The local .knowledge-store.json file tells future scripts which persistent store to search.
Did indexing fail?
Confirm that GEMINI_API_KEY is still available in the PowerShell terminal from earlier. Open a new VS Code terminal if the environment variable was added after the current terminal started.
Check that knowledge/atlas.md exists at the exact path used by ingest.js. The catch block removes the partial store when an upload attempt fails.
Help me diagnose why ingest.js did not index the Project Atlas note.
Your private source is indexed and ready for retrieval. Next, you will ground answers in this store and print the citations that support them.
Ground Answers and Show Citations
Your Project Atlas note now lives in a persistent Gemini File Search store. That indexed source gives the assistant private evidence to retrieve.
The baseline could only guess because it had no access to the note. Grounding connects each answer to retrieved evidence.
This step also prints source citations. It declines questions when the indexed documents contain no supporting fact.
In this step, get ready to:
- Build ask.js to retrieve evidence from the saved File Search store.
- Ask a known-fact question to receive a grounded answer with a citation.
- Add cleanup.js before testing the unsupported-question guardrail.
Build the grounded assistant
The assistant needs the saved store name before it can retrieve your indexed note. It also needs a question from the terminal before it makes an API request.
- In the VS Code Explorer sidebar, create ask.js inside the personal-knowledge-assistant folder.
- Add the imports, question validation, store validation, and Gemini client by copying this code into ask.js:
import { existsSync, readFileSync } from "node:fs";
import { GoogleGenAI } from "@google/genai";
const STORE_FILE = ".knowledge-store.json";
const question = process.argv.slice(2).join(" ").trim();
if (!question) {
throw new Error('Usage: node ask.js "your question"');
}
if (!existsSync(STORE_FILE)) {
throw new Error("No knowledge store found. Run node ingest.js first.");
}
const { name: storeName } = JSON.parse(readFileSync(STORE_FILE, "utf8"));
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
What does this code do?
- The file-system imports let the script confirm that .knowledge-store.json exists before reading it.
- The question variable combines the command-line words into one question.
- The storeName value identifies the persistent store created during indexing.
- The ai client uses the API key already available in your terminal.
- Save ask.js.
- Confirm that ask.js appears beside ingest.js in the VS Code Explorer sidebar.
You now have an input boundary that stops the assistant when its question or store record is missing.
Seeing a problem in the setup code?
Check that the file is named ask.js inside the same folder as .knowledge-store.json.
Compare the capitalization of STORE_FILE and GEMINI_API_KEY with the code above.
Help me check the setup section of ask.js.
The retrieval request supplies two controls. The prompt limits the answer to document evidence. The file_search tool points Gemini at your saved store.
- Place your cursor below the ai client in ask.js.
- Add the grounded interaction by copying this code:
const interaction = await ai.interactions.create({
model: "gemini-3.8-flash",
input: [
"Answer using only evidence retrieved from my uploaded documents.",
'If the documents do not support an answer, reply: "The documents do not contain that information."',
`Question: ${question}`,
].join("\n"),
tools: [
{
type: "file_search",
file_search_store_names: [storeName],
},
],
});
How does grounding happen?
- The input array sets an evidence-only rule before adding the learner's question.
- The refusal sentence gives unsupported questions a clear response.
- The file_search_store_names value connects the request to the resource recorded during ingestion.
- The model receives retrieved document chunks as context before producing its answer.
- Save ask.js.
- Confirm that the request contains the file_search tool below the evidence-only prompt.
Your request now has the retrieval boundary that the ungrounded baseline lacked.
Does the retrieval request look incomplete?
Check that tools sits inside the object passed to ai.interactions.create().
Make sure file_search_store_names uses square brackets around storeName.
Help me inspect the File Search request in ask.js.
A grounded answer is more useful when you can inspect where it came from. The interaction response stores answer text and citation annotations inside model-output steps.
- Place your cursor below the completed interaction request in ask.js.
- Collect the answer text and unique citation details by copying this code:
const answerBlocks = [];
const citations = new Set();
for (const step of interaction.steps ?? []) {
if (step.type !== "model_output") continue;
for (const contentBlock of step.content ?? []) {
if (contentBlock.type !== "text") continue;
answerBlocks.push(contentBlock.text);
for (const annotation of contentBlock.annotations ?? []) {
if (annotation.type !== "file_citation") continue;
const fileName = annotation.file_name || "uploaded document";
const page =
annotation.pageNumber === undefined
? ""
: `, page ${annotation.pageNumber}`;
const source = annotation.source ? `: ${annotation.source}` : "";
citations.add(`${fileName}${page}${source}`);
}
}
}
How are citations collected?
- The outer loop selects response steps produced by the model.
- The next loop collects each text block for the final answer.
- The annotation loop keeps only file_citation entries.
- The Set stores each formatted source once when the same citation appears more than once.
- Save ask.js.
- Confirm that answerBlocks and citations appear below the interaction request.
The script can now separate the model's answer from the source details attached to it.
Are citation fields marked as errors?
Check the exact capitalization of pageNumber and the underscore in file_name.
Make sure each citation is added inside the annotation loop.
Help me debug the citation traversal in ask.js.
The final output keeps the answer and its evidence separate in the terminal. That layout makes a grounded result easy to inspect.
- Place your cursor below the citation traversal in ask.js.
- Print the answer and collected sources by copying this code:
console.log("\nAnswer:\n");
console.log(answerBlocks.join("\n").trim() || interaction.output_text);
console.log("\nSources:");
if (citations.size === 0) {
console.log("No file citation returned.");
} else {
for (const citation of citations) {
console.log(`- ${citation}`);
}
}
What does the terminal output show?
- The first section prints the collected answer text.
- The fallback uses interaction.output_text when the traversal finds no text blocks.
- The source section prints every unique citation on its own line.
- A response without citation annotations prints a clear citation status.
- Save ask.js.
- Confirm that the final line in ask.js closes the citation output block.
Seeing mismatched brackets or quotes?
Compare the closing braces after the citation loop with the code above. A missing brace can make VS Code underline the end of the file.
Check that the newline characters use \n inside quoted strings.
Help me find a syntax problem in ask.js.
✔️ Awesome, I've got everything!
Great. Double-check that you saved ask.js before testing it.
ⓧ I'd like to double check the full code
import { existsSync, readFileSync } from "node:fs";
import { GoogleGenAI } from "@google/genai";
const STORE_FILE = ".knowledge-store.json";
const question = process.argv.slice(2).join(" ").trim();
if (!question) {
throw new Error('Usage: node ask.js "your question"');
}
if (!existsSync(STORE_FILE)) {
throw new Error("No knowledge store found. Run node ingest.js first.");
}
const { name: storeName } = JSON.parse(readFileSync(STORE_FILE, "utf8"));
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const interaction = await ai.interactions.create({
model: "gemini-3.8-flash",
input: [
"Answer using only evidence retrieved from my uploaded documents.",
'If the documents do not support an answer, reply: "The documents do not contain that information."',
`Question: ${question}`,
].join("\n"),
tools: [
{
type: "file_search",
file_search_store_names: [storeName],
},
],
});
const answerBlocks = [];
const citations = new Set();
for (const step of interaction.steps ?? []) {
if (step.type !== "model_output") continue;
for (const contentBlock of step.content ?? []) {
if (contentBlock.type !== "text") continue;
answerBlocks.push(contentBlock.text);
for (const annotation of contentBlock.annotations ?? []) {
if (annotation.type !== "file_citation") continue;
const fileName = annotation.file_name || "uploaded document";
const page =
annotation.pageNumber === undefined
? ""
: `, page ${annotation.pageNumber}`;
const source = annotation.source ? `: ${annotation.source}` : "";
citations.add(`${fileName}${page}${source}`);
}
}
}
console.log("\nAnswer:\n");
console.log(answerBlocks.join("\n").trim() || interaction.output_text);
console.log("\nSources:");
if (citations.size === 0) {
console.log("No file citation returned.");
} else {
for (const citation of citations) {
console.log(`- ${citation}`);
}
}
Ask a question your notes can answer
The indexed note contains two precise facts that the baseline could not reliably know. This question checks whether retrieval changes that result.
Before you run the question, which budget and deadline do you expect the assistant to retrieve?
- Ask for the Project Atlas budget ceiling and pilot deadline by running this command in the VS Code PowerShell terminal:
node ask.js "What are Project Atlas's budget ceiling and pilot deadline?"
What should you see?
The answer reports a budget ceiling of $4,800 and a pilot deadline of October 18, 2026.
Under Sources:, you see a citation that identifies Project Atlas notes.
That is the key retrieval win. Your assistant can now answer a private question from the indexed note while showing its evidence.
Missing the answer or citation?
Confirm that the earlier indexing command completed successfully. The .knowledge-store.json file must still exist inside personal-knowledge-assistant.
Check that file_search_store_names contains storeName in ask.js.
Help me troubleshoot a grounded answer with a missing fact or citation.
Verify the refusal and add cleanup
The File Search store persists after the script exits. A cleanup script gives that cloud resource a deliberate deletion path when you finish using it.
- In the VS Code Explorer sidebar, create cleanup.js inside the personal-knowledge-assistant folder.
- Add the store deletion workflow by copying this code into cleanup.js:
import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { GoogleGenAI } from "@google/genai";
const STORE_FILE = ".knowledge-store.json";
if (!existsSync(STORE_FILE)) {
console.log("No local knowledge store record was found.");
} else {
const { name: storeName } = JSON.parse(readFileSync(STORE_FILE, "utf8"));
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
await ai.fileSearchStores.delete({
name: storeName,
config: { force: true },
});
unlinkSync(STORE_FILE);
console.log(`Deleted ${storeName} and removed ${STORE_FILE}.`);
}
How does cleanup work?
- The existence check prevents the script from reading a missing local store record.
- The saved resource name tells the API which File Search store to delete.
- The deletion request uses force: true to remove the store with its indexed documents.
- The local record is removed only after the cloud deletion succeeds.
- Save cleanup.js.
- Confirm that cleanup.js appears beside ask.js in the VS Code Explorer sidebar.
- Keep the saved store in place for the optional Secret Mission.
Your project now includes a repeatable cleanup path for the persistent store.
Does the cleanup file show an error?
Check that unlinkSync appears in the import from node:fs.
Confirm that the store deletion finishes before unlinkSync(STORE_FILE) removes the local record.
Help me inspect cleanup.js without deleting my store yet.
✔️ Awesome, I've got everything!
Your cleanup path is saved. Leave the store intact while you complete the final guardrail check.
ⓧ I'd like to double check the full code
import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { GoogleGenAI } from "@google/genai";
const STORE_FILE = ".knowledge-store.json";
if (!existsSync(STORE_FILE)) {
console.log("No local knowledge store record was found.");
} else {
const { name: storeName } = JSON.parse(readFileSync(STORE_FILE, "utf8"));
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
await ai.fileSearchStores.delete({
name: storeName,
config: { force: true },
});
unlinkSync(STORE_FILE);
console.log(`Deleted ${storeName} and removed ${STORE_FILE}.`);
}
A trustworthy assistant must also recognize missing evidence. The logo-color question targets a fact that never appears in knowledge/atlas.md.
Before you run the final check, do you expect the assistant to invent a color or follow its refusal rule?
- Test the evidence boundary by running this command in the VS Code PowerShell terminal:
node ask.js "What color is the Project Atlas logo?"
What should you see?
The answer says The documents do not contain that information. This matches the refusal rule supplied with the question.
The source section may report No file citation returned. because the store contains no evidence for a logo color.
You have closed the grounding loop. The assistant retrieves known facts with citations and declines unsupported claims.
Did the assistant invent a logo color?
Compare the refusal instruction in ask.js with the full-code reference. The sentence must remain inside the request's input array.
Confirm that the question refers to the Project Atlas logo. A different question may match a fact that is present in the note.
Help me debug an unsupported answer that ignored the refusal rule.
Your command-line assistant now answers from Project Atlas evidence, exposes its sources, and handles missing facts safely. The persistent store remains available for the optional Secret Mission.
Secret mission
Filter Answers by Knowledge Category
A larger knowledge base can contain facts from several parts of your life. In this mission, you will add category metadata to each document so the same assistant can search work notes, learning notes, or every category.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep the project ready, pause your API activity, or remove every resource. Your local files have no ongoing cost.
Cost warning
Keeping the Gemini File Search store does not incur storage charges. Query-time embedding generation is also free.
Initial indexing and model tokens follow Gemini API tier and pricing rules. Paid-tier usage can incur charges when billing is enabled.
Resources you used:
- The persistent Gemini File Search store containing knowledge/atlas.md plus knowledge/learning.md.
- The local personal-knowledge-assistant folder containing your project files plus installed npm dependencies.
- The Gemini API key stored in your Windows user environment plus managed through Google AI Studio.
Keep everything running
No action is needed. Choose this when you want to keep testing category filters or add more non-sensitive notes.
- Keep the personal-knowledge-assistant folder available in Visual Studio Code.
- Retain .knowledge-store.json so ask.js can find the existing File Search store.
- Keep GEMINI_API_KEY in your Windows user environment.
- Review the cloud-data terms before uploading personal documents.
Pause - I'll come back to this later
Stop making API requests while preserving the store for another session. No Node.js process needs to remain running.
- Make no further requests with ask.js or ingest.js while paused.
- Keep .knowledge-store.json inside personal-knowledge-assistant.
- Keep GEMINI_API_KEY available in your Windows user environment.
- Return to the personal-knowledge-assistant folder in Visual Studio Code when you are ready to continue.
Delete - I don't want to use this again
Deleting the store permanently removes its indexed data. Your Markdown source notes stay in the local project until the final folder cleanup.
- Delete the persistent Gemini File Search store by running this command in the PowerShell terminal from earlier:
node cleanup.js
What Does This Cleanup Do?
The cleanup.js script reads the saved store name from .knowledge-store.json. It uses that name to delete the persistent File Search store.
The script then removes .knowledge-store.json. Your Markdown source files stay untouched.
- Confirm the terminal prints Deleted <store-name> and removed .knowledge-store.json..
Store Not Deleted?
Confirm the VS Code terminal is still inside the personal-knowledge-assistant folder. Confirm .knowledge-store.json exists before retrying.
Help me troubleshoot the cleanup command without exposing my API key.
That takes care of the cloud resource: your indexed notes are no longer stored in the File Search store.
Clean Up Both Key Locations
Credential cleanup can feel fiddly because the key appears in two places.
Windows stores the GEMINI_API_KEY environment variable. Google AI Studio manages the API key itself.
- Search for Environment Variables using Windows search.
- Open Environment Variables in System Properties.
- Select GEMINI_API_KEY from the user variables.
- Remove the selected user variable.
- Confirm GEMINI_API_KEY no longer appears in the user variables.
- Return to Google AI Studio.
- Delete the Gemini API key if you no longer need it.
- Confirm the deleted key is no longer listed.
Your cloud data plus credential are now handled. The final cleanup removes the local files plus installed dependencies.
- Close Visual Studio Code.
- Use the Windows file browser to delete the personal-knowledge-assistant folder from the location where you saved it.
- Confirm the personal-knowledge-assistant folder no longer appears in that location.
Nice Work!
Nice Work!
You did it! You've built a command-line personal knowledge assistant with Node.js powered by Gemini File Search. It grounds each answer in indexed notes with inspectable citations.
You've learned how to:
- Compare an ungrounded baseline with retrieval-augmented generation. This reveals why private facts need indexed evidence.
- Index the Project Atlas notes in a persistent File Search store. Use embeddings to make those notes searchable by meaning.
- Print source citations for supported answers. Enforce a prompt guardrail that declines questions without evidence.
- Secret Mission: Scope retrieval to work, learning, or all with metadata filtering.
Ready to quiz yourself?