A Streamlit application that reduces AI language model costs by retrieving only the document sections relevant to a query, instead of sending the entire document as context.
- Overview
- How It Works
- Architecture
- Getting Started
- Project Structure
- Usage Guide
- Technology Stack
- Configuration
- Deployment
- Performance
- Environment Impact
When processing large documents with AI models, organizations typically pass the entire document as context β leading to high token consumption, inflated API costs, and slower inference. Groot solves this by using semantic vector search to extract only the chunks of a document that are relevant to a specific query, reducing context size by up to 96% while maintaining response quality.
When you upload a PDF, Groot reads it page by page using pypdf and concatenates all the extracted text into a single string β the raw full document. This full text is what a naive LLM integration would send directly to the model, costing tens of thousands of tokens on every query.
Instead, Groot immediately splits that text into overlapping chunks using LangChain's RecursiveCharacterTextSplitter. Each chunk is roughly 500 words (β2,500 characters) with a 100-word overlap between adjacent chunks. The overlap ensures that a sentence split across a chunk boundary still appears fully in at least one chunk, so no fact gets lost at the seam.
Every chunk is then passed through all-MiniLM-L6-v2, a local SentenceTransformer model that runs entirely on the server with no API calls. It converts each chunk into a 384-dimensional float32 vector β a point in mathematical space where semantically similar text lands close together. All these vectors are loaded into a FAISS IndexFlatL2, an in-memory index that can find the nearest neighbours to any query vector in under a millisecond regardless of document size. This entire indexing process runs in a background thread so the UI stays responsive.
When you type a query, Groot does not simply embed it and search once. It first runs query expansion: it strips instruction words like "summarize" or "explain" to expose the core topic, then uses a regex to extract any conditional clauses (phrases starting with when, if, while, during, etc.) as separate sub-queries β because the document likely uses that exact conditional phrasing in the answer. It also adds a keyword-only fallback. The result is 2β5 search strings from a single user query.
Each sub-query is independently embedded and searched against the FAISS index, fetching a pool of top_k Γ 5 candidates. All results are merged and deduplicated by chunk position. The candidates are then re-ranked: for every query keyword that appears literally in a chunk's text, its effective distance is reduced by 0.08. This means a chunk containing the exact domain terms from the query floats above a chunk that is only semantically similar β which is what catches specific operational facts that pure vector similarity misses. Finally, any candidate with a raw L2 distance above 2.0 is dropped, and the best top_k chunks are returned.
Those chunks β typically 3β8% of the original document's tokens β are joined and sent to Gemini as the context window. The same query is also sent with the full raw document so you can compare responses side by side. Groot displays the token counts, costs, and savings for both paths so the quality-versus-cost tradeoff is immediately visible.
The draw.io source (
groot-core-pipeline.drawio) is also in the repo β open it at diagrams.net or in the draw.io desktop app for an editable version.
- Python 3.11 or higher
- Google Gemini API key (or Vertex AI credentials for Cloud Run)
git clone <repository-url>
cd groot
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS/Linux
source .venv/bin/activate
pip install -r requirements.txtstreamlit run app.pyOpens at http://localhost:8501
groot/
βββ app.py # Main Streamlit application & page router
βββ utils.py # Core retrieval pipeline
β # ββ extract_text_from_pdf
β # ββ chunk_text (LangChain splitter)
β # ββ build_faiss_index
β # ββ search_chunks (multi-stage retrieval)
β # ββ _expand_query (conditional clause extraction)
β # ββ _extract_keywords (stop-word filtered)
β # ββ generate_gemini_response (REST + retry)
β # ββ generate_gemini_vertex (Vertex AI SDK)
β # ββ DocumentProcessorThread (background worker)
βββ requirements.txt
βββ Dockerfile
βββ groot-core-pipeline.svg # Pipeline diagram (renders on GitHub)
βββ groot-core-pipeline.drawio # Editable draw.io source
β
βββ components/
β βββ header.py # Navigation header
β βββ settings.py # Settings modal and config state
β
βββ sections/ # Landing page sections
β βββ optimizer.py # Main optimizer tool (page 2)
β βββ hero.py
β βββ technology.py
β βββ cost_savings.py
β βββ connectors.py
β βββ environment.py
β βββ integration.py
β βββ footer_cta.py
β
βββ image/
β βββ groot-logo.png
β βββ groot-logo_old.png
β
βββ resources/ # Sample PDFs for testing
β βββ Indian Paneer recipies.pdf
β βββ vanguards_principles_for_investing_success.pdf
β
βββ .github/workflows/
βββ deploy.yml # Build and deploy to Cloud Run
βββ uninstall.yml # Tear down Cloud Run service
Step 1 β Upload a PDF via the optimizer page. A progress bar tracks extraction β chunking β embedding β indexing.
Step 2 β Enter a query, for example:
- "Summarize the main risk factors"
- "fup copy command when source file is open"
Step 3 β Click "Optimize and Compare". Groot retrieves the relevant chunks and generates two responses in parallel β one using the full document, one using only the retrieved context.
Step 4 β Review results. Section 2 shows token counts and cost savings. Section 3 shows the side-by-side LLM responses.
Step 5 β Tune settings via βοΈ Settings if needed (see Configuration).
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Streamlit | Web UI framework |
| Text Splitting | LangChain RecursiveCharacterTextSplitter |
Semantic-aware chunking |
| Embeddings | all-MiniLM-L6-v2 (Sentence-Transformers) |
384-dim local embeddings, <2GB RAM |
| Vector DB | FAISS IndexFlatL2 |
Sub-millisecond L2 similarity search |
| Retrieval | Multi-query expansion + keyword re-ranking | High-recall factual retrieval |
| LLM (local) | Google Gemini REST API | Response generation with 4Γ retry |
| LLM (cloud) | gemini-2.5-flash via Vertex AI SDK |
Cloud Run production backend |
| PDF Processing | pypdf | Page-by-page text extraction |
| Tokenization | tiktoken cl100k_base |
Token counting |
| Numerics | NumPy | Embeddings and distance math |
| Concurrency | threading.Thread |
Non-blocking document processing |
All settings are stored in st.session_state and accessible via the βοΈ Settings button in the app.
| Parameter | Default | Guidance |
|---|---|---|
| Chunk Size | 500 words | Larger = more context per chunk, fewer chunks. 300β700 works well. |
| Chunk Overlap | 100 words | Prevents facts from being split at boundaries. 50β150 recommended. |
| Top-K | 8 | More chunks = better recall but higher token count. 5β12 is the useful range. |
| Cost/1M tokens | $3.50 | Set to your actual API tier pricing for accurate savings display. |
| Constant | Value | Purpose |
|---|---|---|
KEYWORD_BONUS |
0.08 | L2 distance reduction per matched keyword during re-ranking |
SIMILARITY_THRESHOLD |
2.0 | Maximum L2 distance to keep a chunk (falls back to raw top_k if all filtered) |
| Candidate pool | 5Γ top_k | FAISS fetch size per sub-query before re-ranking |
| Embedding model | all-MiniLM-L6-v2 |
<2GB RAM β safe on Cloud Run 4 GiB instances |
docker build -t groot:latest .
docker run -p 8080:8080 groot:latest
# with API key:
docker run -p 8080:8080 -e GOOGLE_API_KEY=your_key_here groot:latestThe app uses Workload Identity when backend is set to "Vertex AI (Cloud Run)" β no API key needed in production:
client = genai.Client(vertexai=True, project="singla", location="europe-west3")CI/CD is handled by .github/workflows/deploy.yml (build + deploy) and uninstall.yml (teardown), both triggered via workflow_dispatch.
| Metric | Unoptimized | Optimized | Improvement |
|---|---|---|---|
| Tokens/Query | 135,000 | 5,000 | ~96% β |
| Cost/Query | $0.473 | $0.018 | ~96% β |
| Vector Search | β | <1ms | Sub-millisecond |
| Response Quality | Baseline | Maintained | β |
Processing time: <5s for most documents, <30s for 500+ page documents.
Each 96% token reduction directly translates to proportional savings in GPU cycles, inference energy, and COβ at the data centre. At scale (1M queries/day on a 135k-token document), the compounded savings are substantial.
Areas for improvement:
- Support for DOCX, TXT, HTML formats
- Multi-document search across collections
- Cross-encoder re-ranker for higher precision
- REST API endpoints
- Caching layer for frequently searched documents
"API Key not found" β Click βοΈ Settings and enter your Gemini API key from Google AI Studio.
"Could not extract text from PDF" β The PDF must be text-based, not a scanned image. Use an OCR tool first.
"FAISS installation error" β pip install faiss-cpu works on all platforms including Apple Silicon.
Optimized response missing specific details β Increase Top-K in Settings (try 10β15).
- Streamlit β Web framework
- FAISS β Vector search
- Sentence-Transformers β
all-MiniLM-L6-v2embeddings - LangChain β Text splitting
- Google AI β Gemini LLM
- Email: harishsingla89@gmail.com
- Issues: GitHub issue tracker
Made with πΏ for a smarter, greener AI future.