Google File Search RAG Pipeline
Build an async RAG pipeline over a directory of files with Gemini File Search.
The source snapshot treats a completed upload operation as success without checking its error, and cleanup is not guaranteed on failures. Use the current adaptation below. It validates local inputs before creating stores, checks indexing errors, and attempts cleanup of its own stores in finally.
"""
Google File Search Rag Pipeline
===============================
Cookbook example for `google/gemini/file_search_rag_pipeline.py`.
"""
import asyncio
from pathlib import Path
from agno.agent import Agent
from agno.models.google import Gemini
# ---------------------------------------------------------------------------
# Create Agent
# ---------------------------------------------------------------------------
# Configuration
DOCUMENTS_DIR = (
Path(__file__).parent / "documents"
) # Use documents directory in same folder
STORE_NAME = "RAG Pipeline Demo"
async def create_and_populate_store(model: Gemini, documents_dir: Path):
"""Create a File Search store and upload all documents from a directory."""
print(f"Creating File Search store: {STORE_NAME}")
store = await model.async_create_file_search_store(display_name=STORE_NAME)
print(f"[OK] Store created: {store.name}")
# Find all supported documents
supported_extensions = [".txt", ".pdf", ".md", ".json", ".py", ".js", ".ts"]
files = [
f
for f in documents_dir.glob("**/*")
if f.suffix.lower() in supported_extensions
]
print(f"\nFound {len(files)} documents to upload")
# Upload files with progress tracking
upload_operations = []
for i, file_path in enumerate(files, 1):
print(f" [{i}/{len(files)}] Uploading {file_path.name}...")
# Determine chunking config based on file type
chunking_config = None
if file_path.suffix in [".py", ".js", ".ts"]:
# Code files - smaller chunks for precise retrieval
chunking_config = {
"white_space_config": {
"max_tokens_per_chunk": 150,
"max_overlap_tokens": 30,
}
}
else:
# Documentation files - larger chunks for context
chunking_config = {
"white_space_config": {
"max_tokens_per_chunk": 300,
"max_overlap_tokens": 50,
}
}
# Metadata based on file properties
metadata = [
{"key": "filename", "string_value": file_path.name},
{"key": "extension", "string_value": file_path.suffix},
{"key": "size_kb", "numeric_value": file_path.stat().st_size // 1024},
]
operation = await model.async_upload_to_file_search_store(
file_path=file_path,
store_name=store.name,
display_name=file_path.stem,
chunking_config=chunking_config,
custom_metadata=metadata,
)
upload_operations.append((file_path.name, operation))
# Wait for all uploads to complete
print("\nWaiting for all uploads to complete...")
for filename, operation in upload_operations:
try:
await model.async_wait_for_operation(operation, max_wait=300)
print(f" [OK] {filename} indexed")
except TimeoutError:
print(f" ✗ {filename} timed out")
except Exception as e:
print(f" ✗ {filename} failed: {e}")
return store
async def query_with_citations(model: Gemini, query: str, store_name: str):
"""Query the File Search store and display results with citations."""
print(f"\nQuery: {query}")
print("=" * 80)
# Configure model to use File Search
model.file_search_store_names = [store_name]
# Create agent and get response
agent = Agent(model=model, markdown=True)
run = agent.run(query)
print(f"\nAnswer:\n{run.content}")
# Extract and display citations directly from run.citations
sources = []
chunks = []
if run.citations and run.citations.raw:
grounding_metadata = run.citations.raw.get("grounding_metadata", {})
grounding_chunks = grounding_metadata.get("grounding_chunks", []) or []
sources_set = set()
for chunk in grounding_chunks:
if isinstance(chunk, dict):
retrieved_context = chunk.get("retrieved_context")
if isinstance(retrieved_context, dict):
title = retrieved_context.get("title", "Unknown")
sources_set.add(title)
chunks.append(
{
"title": title,
"uri": retrieved_context.get("uri", ""),
"text": retrieved_context.get("text", ""),
"type": "file_search",
}
)
sources = sorted(list(sources_set))
if sources:
print("\n" + "─" * 80)
print(f"Sources ({len(sources)} documents):")
for i, source in enumerate(sources, 1):
print(f" [{i}] {source}")
if chunks:
print(f"\nCitations ({len(chunks)} chunks):")
for i, chunk in enumerate(chunks[:3], 1): # Show first 3
print(f"\n [{i}] {chunk['title']}")
if chunk.get("text"):
text = chunk["text"]
if len(text) > 150:
text = text[:150] + "..."
print(f' "{text}"')
else:
print("\nNo citations found")
return run, {"sources": sources, "grounding_chunks": chunks}
async def main():
"""Main RAG pipeline execution."""
print("=" * 80)
print("RAG Pipeline with Gemini File Search")
print("=" * 80)
# Check if documents directory exists
if not DOCUMENTS_DIR.exists():
print(f"\n✗ Error: Documents directory not found: {DOCUMENTS_DIR}")
print("Please create the directory and add some documents to index.")
return
# Initialize model
model = Gemini(id="gemini-3.7-flash")
# Step 1: Create and populate store
print("\n" + "=" * 80)
print("Step 1: Creating and populating File Search store")
print("=" * 80)
try:
store = await create_and_populate_store(model, DOCUMENTS_DIR)
except Exception as e:
print(f"\n✗ Error creating store: {e}")
return
# Step 2: List and verify documents
print("\n" + "=" * 80)
print("Step 2: Verifying uploaded documents")
print("=" * 80)
documents = await model.async_list_documents(store.name)
print(f"\n[OK] Total documents in store: {len(documents)}")
print("\nDocuments:")
for doc in documents[:10]: # Show first 10
print(f" - {doc.display_name}")
if len(documents) > 10:
print(f" ... and {len(documents) - 10} more")
# Step 3: Interactive querying
print("\n" + "=" * 80)
print("Step 3: Querying the knowledge base")
print("=" * 80)
queries = [
"What are the main topics covered in the documentation?",
"Can you summarize the key technical concepts?",
"What code examples are available?",
]
all_citations = []
for query in queries:
response, citations = await query_with_citations(model, query, store.name)
all_citations.append(citations)
# Step 4: Citation analysis
print("\n" + "=" * 80)
print("Step 4: Citation Analysis")
print("=" * 80)
all_sources = set()
for citations in all_citations:
all_sources.update(citations["sources"])
print(f"\n[OK] Total unique sources referenced: {len(all_sources)}")
print(f"[OK] Document coverage: {len(all_sources)}/{len(documents)} documents")
# Step 5: Cleanup
print("\n" + "=" * 80)
print("Step 5: Cleanup")
print("=" * 80)
try:
await model.async_delete_file_search_store(store.name, force=True)
print(f"[OK] Store deleted: {store.name}")
except Exception as e:
print(f"✗ Error deleting store: {e}")
print("\n" + "=" * 80)
print("[OK] RAG Pipeline completed successfully!")
print("=" * 80)
# Run the async main function
# ---------------------------------------------------------------------------
# Run Agent
# ---------------------------------------------------------------------------
if __name__ == "__main__":
asyncio.run(main())Current adaptation
File Search stores persist until deleted, subject to embedding-model lifecycle limits. They are separate from the Files API's expiring uploads. These demo scripts create their own stores and use force=True to remove their documents during cleanup. Do not substitute a shared production store name. If cleanup fails, use the reported store name to delete the demo resource after resolving the error.
The operation's done state is not proof of successful indexing: inspect completed.error before querying. Citations depend on the generated answer; an empty citation field is possible.
Save this adaptation as file_search_rag_current.py. The original block above remains the pinned cookbook source.
import asyncio
from pathlib import Path
from agno.agent import Agent
from agno.models.google import Gemini
async def main():
documents = Path("cookbook/90_models/google/gemini/documents")
extensions = {".txt", ".pdf", ".md", ".json", ".py", ".js", ".ts"}
files = [p for p in documents.rglob("*") if p.is_file() and p.suffix.lower() in extensions]
if not files:
raise ValueError(f"Add documents to {documents} before running")
model = Gemini(id="gemini-3.7-flash")
store = await model.async_create_file_search_store(display_name="RAG Demo")
try:
for path in files:
operation = await model.async_upload_to_file_search_store(
file_path=path, store_name=store.name,
custom_metadata=[{"key": "filename", "string_value": path.name}],
)
completed = await model.async_wait_for_operation(operation, max_wait=300)
if completed.error:
raise RuntimeError(f"Indexing {path.name} failed: {completed.error}")
model.file_search_store_names = [store.name]
agent = Agent(model=model, markdown=True)
for query in ["What are the main topics?", "Summarize the key technical concepts."]:
result = await agent.arun(query)
print(result.content)
print(result.citations)
finally:
await model.async_delete_file_search_store(store.name, force=True)
if __name__ == "__main__":
asyncio.run(main())Run the Example
Set up your virtual environment
uv venv --python 3.12
source .venv/bin/activateInstall dependencies
uv pip install -U agno google-genaiExport your Google API key
export GOOGLE_API_KEY="your_google_api_key_here"Clone Agno
Clone the pinned Agno source and run the remaining commands from its root:
git clone https://github.com/agno-agi/agno.git
cd agno
git checkout 8f36eaf2d18e91afa7b327eec66a3cd3685dcb87Prepare your document corpus
Add supported documents under cookbook/90_models/google/gemini/documents. The checked-in test notes are not a prepared knowledge base; replace them with the content you want to query before uploading.
Run the example
Save the current adaptation as file_search_rag_current.py at the repository root, then run:
python file_search_rag_current.pyFull source: cookbook/90_models/google/gemini/file_search_rag_pipeline.py