A document-grounded Retrieval-Augmented Generation (RAG) chatbot for technical support teams working with solar and energy equipment manuals.
The one core feature this project must have to work is:
A user can upload a technical manual, ask a question about it, and receive either a document-grounded answer with visible source evidence or a clear refusal when the uploaded documents do not contain enough information.
Everything else supports this core feature: document upload, text extraction, chunking, embeddings, retrieval, answer generation, citations, source cards, and refusal behavior.
For the MVP, the system is successful when it can:
- Upload at least one PDF, DOCX, TXT, or Markdown technical document.
- Extract and chunk the document text.
- Store searchable chunks in a persistent vector database.
- Let a user ask a question from the Streamlit UI.
- Retrieve relevant chunks from indexed documents.
- Generate an answer grounded in retrieved evidence.
- Show source cards with filename, page where available, chunk ID, relevance score, and supporting snippet.
- Refuse to answer when the indexed documents do not provide enough evidence.
- Project Overview
- App Screenshots
- Problem Statement
- Dataset
- Key Features
- Technology Stack
- Architecture
- Project Structure
- Prerequisites
- Environment Variables
- Local Setup With uv
- Running the App
- API Reference
- How the RAG Pipeline Works
- Grounding and Provenance
- Hallucination Controls
- Fault-Code Exact Lookup
- Local Response Caching
- Testing and Quality Checks
- Docker Usage
- Demo Flow
- Team Workflow
- Acceptance Criteria
- Limitations
- Future Improvements
- Troubleshooting
- License
Engineers and field technicians often search through large inverter, battery, PV module, fan, charge controller, and maintenance manuals to find fault codes, alarm meanings, troubleshooting steps, and safety instructions.
This project provides a trustworthy technical-support assistant that can:
- Upload and index technical manuals.
- Retrieve relevant document passages.
- Answer questions using retrieved evidence.
- Show citations and source cards for verification.
- Refuse unsupported answers in document mode.
- Preserve document provenance such as filename, page, section, chunk ID, relevance score, and supporting text.
- Provide a fast exact fault-code lookup that does not call the LLM.
The goal is not just to "chat with a PDF". The goal is to build a transparent document question-answering system that can be extended toward real-world technical support.
The screenshots below show the main Streamlit interface and the core workflows used in the demo.
| Upload and Indexing | Chat With Sources |
|---|---|
![]() |
![]() |
| Fault-Code Lookup |
|---|
![]() |
Traditional manual search is slow and error-prone. Engineers may spend significant time looking for one fault code across large manufacturer PDFs or maintenance documents.
A normal LLM chatbot can sound confident even when it is wrong. For technical support, that is risky. This project reduces that risk by grounding answers in uploaded documents and exposing the evidence used to answer.
When the indexed documents do not contain enough information, the assistant should say:
I could not find enough information in the uploaded documents to answer this reliably.
This project does not require a traditional CSV or numerical dataset for the MVP.
The dataset is a document corpus: a collection of solar and energy technical documents such as inverter manuals, PV module manuals, battery manuals, fan manuals, charge controller manuals, troubleshooting guides, fault-code tables, maintenance instructions, FAQs, and technical notes.
The system extracts text from these documents, splits the text into chunks, embeds the chunks, and stores them in a vector database. Each chunk keeps provenance metadata so answers can point back to the exact source evidence.
- Streamlit web UI for document upload, chat, source cards, and fault-code lookup.
- FastAPI backend with a modular RAG pipeline.
- uv-managed Python projects for both backend and frontend.
- Upload support for PDF, DOCX, TXT, Markdown, and MD files.
- PDF page-number preservation where text extraction supports it.
- Text cleaning and chunking with overlap.
- Persistent vector database using ChromaDB by default.
- Configurable embeddings.
- OpenAI-compatible LLM provider support.
- Mock generator and hash embeddings for local tests.
- Document mode, general mode, and hybrid mode.
- Default
ALLOW_GENERAL_KNOWLEDGE=trueso explicit General mode can answer, while Document mode remains strictly source-grounded. - Citation validation.
- Exact fault-code lookup endpoint and table view.
- Local response cache to reduce repeated LLM API calls.
- Backend tests with pytest.
- Ruff linting and formatting.
- Optional Docker support.
- Python 3.11+
- uv for dependency management and execution
- FastAPI
- Uvicorn
- Pydantic
- pydantic-settings
- python-dotenv
- ChromaDB
- SentenceTransformers or hash/mock embeddings
- pypdf
- python-docx
- Markdown document support
- pytest
- ruff
- Python
- Streamlit
- httpx
- uv
- Docker
- Docker Compose
- Groq, OpenAI, or another OpenAI-compatible LLM provider
User
|
v
Streamlit Frontend
| Upload documents
| Ask questions
| Lookup fault codes
v
FastAPI Backend
|
+-- Upload API
| +-- Validate file type and size
| +-- Save uploaded document safely
| +-- Extract text
| +-- Clean text
| +-- Chunk text
| +-- Embed chunks
| +-- Store vectors and metadata
|
+-- Chat API
| +-- Retrieve relevant chunks
| +-- Apply similarity threshold
| +-- Build grounded prompt
| +-- Generate answer
| +-- Validate citations
| +-- Return answer and provenance
|
+-- Fault-Code Lookup API
+-- Normalize code variants
+-- Search exact terms in indexed chunks
+-- Return matching table rows without an LLM call
Persistent Storage
|
+-- backend/data/raw Uploaded files
+-- backend/data/processed Document registry and response cache
+-- backend/data/vector_store ChromaDB vector store
rag-chatbot-streamlit/
|
+-- README.md Main project documentation.
+-- CONTRIBUTING.md Collaboration guidelines.
+-- LICENSE.md Project license.
+-- .env.example Example environment variables with no real secrets.
+-- .gitignore Files and folders Git should ignore.
+-- docker-compose.yml Optional Docker Compose setup.
|
+-- backend/ FastAPI backend and RAG pipeline.
| +-- pyproject.toml uv project and backend dependencies.
| +-- uv.lock Locked backend dependency versions.
| +-- Dockerfile Optional backend Docker image.
| +-- README.md Backend-specific notes.
| +-- app/
| | +-- main.py FastAPI app factory and route registration.
| | +-- api/
| | | +-- chat.py POST /chat endpoint.
| | | +-- documents.py GET /documents endpoint.
| | | +-- fault_codes.py POST /fault-codes/lookup endpoint.
| | | +-- upload.py POST /documents/upload endpoint.
| | +-- core/
| | | +-- config.py Environment-backed settings.
| | | +-- dependencies.py Shared RAG service dependency.
| | | +-- logging.py Logging configuration.
| | +-- models/
| | | +-- schemas.py Pydantic request and response models.
| | +-- rag/
| | | +-- chunker.py Converts extracted text into metadata-rich chunks.
| | | +-- embeddings.py Embedding provider implementations.
| | | +-- generator.py LLM and mock generation providers.
| | | +-- loader.py PDF, DOCX, TXT, and Markdown text loading.
| | | +-- prompt_builder.py Grounded and general prompt builders.
| | | +-- provenance.py SourceReference construction.
| | | +-- registry.py Indexed document registry.
| | | +-- response_cache.py SQLite response cache.
| | | +-- retriever.py Semantic retrieval and exact-code term extraction.
| | | +-- service.py Main orchestration layer.
| | | +-- types.py Internal dataclasses.
| | | +-- vector_store.py ChromaDB and in-memory vector store.
| | | +-- verifier.py Refusal text and citation validation.
| | +-- utils/
| | +-- text_cleaning.py Text normalization and snippets.
| +-- data/ Runtime data; ignored by Git.
| +-- tests/
| +-- conftest.py Test app fixture and test settings.
| +-- test_api.py API tests.
| +-- test_rag_units.py RAG unit tests.
|
+-- frontend/ Streamlit frontend.
| +-- pyproject.toml uv project and frontend dependencies.
| +-- uv.lock Locked frontend dependency versions.
| +-- Dockerfile Optional frontend Docker image.
| +-- README.md Frontend-specific notes.
| +-- app.py Streamlit UI.
| +-- .streamlit/
| +-- config.toml Streamlit theme/server config.
|
+-- docs/
+-- acceptance_criteria.md Completion checklist.
+-- architecture.md Architecture notes.
+-- demo_flow.md Suggested demo script.
+-- evaluation.md Evaluation plan and question types.
+-- team_plan.md Team work plan.
Install the following:
- Python 3.11 or newer
- uv
- Git
- Optional: Docker and Docker Compose
- Optional: an LLM API key, such as Groq or an OpenAI-compatible provider
Check uv:
uv --versionIf uv is not installed, follow the official uv installation guide:
https://docs.astral.sh/uv/
Copy the example file:
cp .env.example .env
cp .env.example backend/.envImportant settings:
LLM_PROVIDER=mock
LLM_API_KEY=
LLM_MODEL=gpt-4o-mini
LLM_API_BASE=https://api.openai.com/v1
EMBEDDING_PROVIDER=sentence-transformers
EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
VECTOR_STORE_PROVIDER=chroma
VECTOR_STORE_PATH=
VECTOR_COLLECTION_NAME=technical_manual_chunks
CHUNK_SIZE=750
CHUNK_OVERLAP=150
TOP_K=5
SIMILARITY_THRESHOLD=0.35
ALLOW_GENERAL_KNOWLEDGE=true
RESPONSE_CACHE_ENABLED=true
RESPONSE_CACHE_TTL_SECONDS=604800
MAX_UPLOAD_SIZE_MB=25
ALLOWED_FILE_TYPES=.pdf,.docx,.txt,.md,.markdownFor a Groq-style OpenAI-compatible setup, configure:
LLM_PROVIDER=openai
LLM_API_KEY=your_api_key_here
LLM_API_BASE=https://api.groq.com/openai/v1
LLM_MODEL=llama-3.1-8b-instantNever commit real API keys.
This project intentionally uses uv instead of python -m venv, pip install, or plain python script.py workflows.
cd backend
uv synccd frontend
uv syncStart the backend in one terminal:
cd backend
uv run uvicorn app.main:app --reloadCheck that the backend is alive:
curl http://localhost:8000/energyExpected response:
{
"status": "ok"
}Start the frontend in a second terminal:
cd frontend
uv run streamlit run app.pyOpen the Streamlit UI:
http://localhost:8501
The frontend expects the backend at:
http://localhost:8000
You can change this in the Streamlit sidebar using the Backend URL input.
Health/status endpoint.
curl http://localhost:8000/energyResponse:
{
"status": "ok"
}Uploads and indexes a document.
curl -X POST http://localhost:8000/documents/upload \
-F "file=@sample_manual.pdf"Example response:
{
"document_id": "doc_123",
"filename": "sample_manual.pdf",
"chunks_created": 42,
"status": "indexed"
}Lists indexed documents.
curl http://localhost:8000/documentsExample response:
[
{
"document_id": "doc_123",
"filename": "sample_manual.pdf",
"upload_time": "2026-05-22T10:00:00+00:00",
"chunks_created": 42,
"status": "indexed",
"source_path": "data/raw/doc_123_sample_manual.pdf"
}
]Asks a grounded question.
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"question": "What does fault code 07 mean?",
"top_k": 5,
"mode": "document"
}'Example grounded response:
{
"answer": "Fault code 07 indicates overload timeout and the manual recommends reducing the connected load before restarting the inverter [Source 1].",
"grounded": true,
"confidence": "high",
"mode": "document",
"sources": [
{
"source_id": "Source 1",
"document_id": "doc_123",
"filename": "voltronic_manual.pdf",
"page": 34,
"section": "Fault Codes",
"chunk_id": "doc_123_page_34_chunk_2",
"relevance_score": 0.87,
"text_snippet": "Fault code 07: Overload timeout...",
"full_text": "Fault code 07: Overload timeout. Recommended action..."
}
],
"warnings": [
"For high-risk electrical work, follow manufacturer safety instructions and qualified technician procedures."
]
}Example refusal response:
{
"answer": "I could not find enough information in the uploaded documents to answer this reliably.",
"grounded": false,
"confidence": "low",
"mode": "document",
"sources": [],
"warnings": [
"No retrieved chunk passed the relevance threshold."
]
}Performs exact fault-code matching without an LLM call.
curl -X POST http://localhost:8000/fault-codes/lookup \
-H "Content-Type: application/json" \
-d '{
"code": "07",
"top_k": 10
}'Example response:
{
"code": "07",
"normalized_terms": [
"Fault code 07",
"Error code 07",
"Alarm code 07",
"F07",
"07"
],
"matches": [
{
"source_id": "Match 1",
"document_id": "doc_123",
"filename": "voltronic_manual.txt",
"page": null,
"section": "Fault Codes",
"chunk_id": "doc_123_page_0_chunk_1",
"relevance_score": 0.97,
"matched_terms": [
"Fault code 07",
"07"
],
"text_snippet": "Fault code 07: Overload timeout...",
"full_text": "Fault code 07: Overload timeout. Recommended action..."
}
],
"warnings": []
}- A user uploads a supported technical document.
- The backend validates the file type and size.
- The file is saved under
backend/data/raw/. - The loader extracts text:
- PDF pages are extracted with page metadata where available.
- DOCX files are read paragraph by paragraph.
- TXT, MD, and Markdown files are read as text documents.
- Text is cleaned to reduce whitespace and formatting noise.
- Text is split into overlapping chunks.
- Each chunk receives metadata:
- document ID
- filename
- page number where available
- section heading where detected
- chunk ID
- source path
- upload time
- Chunks are embedded.
- Chunks and metadata are stored in ChromaDB.
- At question time, the retriever finds relevant chunks.
- The prompt builder creates a strict grounded prompt.
- The generator produces an answer.
- The verifier checks citation integrity.
- The API returns the answer plus structured provenance.
Every factual document-mode answer should be supported by retrieved sources.
The backend returns:
answergroundedconfidencemodesourceswarnings
Each source includes:
- source ID
- document ID
- filename
- page number if available
- section if available
- chunk ID
- relevance score
- snippet
- full chunk text
The frontend displays this information as evidence cards and expandable source details so users can verify the answer.
The system includes several safeguards:
- Document mode is the default.
- General mode is enabled by default through
ALLOW_GENERAL_KNOWLEDGE=true, but Document mode still refuses answers without retrieved evidence. - If retrieval returns no strong evidence, the assistant refuses.
- Retrieved chunks are filtered using a similarity threshold.
- The prompt instructs the LLM to answer only from provided context.
- Citation validation removes unsupported source markers.
- Source metadata is taken from the indexed document store, not invented by the LLM.
- Safety warnings are included for high-risk electrical work.
- Exact fault-code lookup can be used before generation.
Technical manuals often use precise labels such as:
07F07Fault code 07Error 07Alarm 07
Semantic retrieval can miss exact labels, especially short codes. The app includes a dedicated exact lookup flow:
- The user enters a fault code in the Streamlit panel.
- The backend normalizes common variants.
- The vector store performs keyword matching against indexed chunks.
- Matching rows are returned without calling the LLM.
- The UI shows a table with file, page, section, chunk ID, score, matched terms, and snippet.
This is useful for quick verification and for saving LLM API usage.
Repeated chat questions can consume unnecessary LLM calls. The backend includes a local SQLite response cache.
Cache settings:
RESPONSE_CACHE_ENABLED=true
RESPONSE_CACHE_TTL_SECONDS=604800The cache key includes:
- normalized question
- answer mode
- top_k
- indexed document fingerprint
- LLM settings
- embedding settings
- similarity threshold
- general knowledge setting
This means cached responses are automatically invalidated when the indexed document set or key settings change.
The cache is stored under:
backend/data/processed/response_cache.sqlite3
When a response comes from cache, the backend adds a warning:
Returned from local response cache; no LLM API call was made.
Run backend tests:
cd backend
uv run pytestRun linting:
cd backend
uv run ruff check .Check formatting:
cd backend
uv run ruff format --check .Apply formatting:
cd backend
uv run ruff format .Check Streamlit syntax:
cd frontend
uv run python -m py_compile app.pyCurrent backend test coverage includes:
- status endpoint
- document upload
- unsupported file type handling
- empty document handling
- corrupted PDF handling
- document listing
- chat response format
- refusal behavior when no documents are indexed
- general knowledge disabled behavior
- exact fault-code lookup
- RAG unit tests
Docker is optional. Local development should use uv directly.
Run both services with Docker Compose:
docker compose up --buildBackend:
http://localhost:8000
Frontend:
http://localhost:8501
Use Docker when:
- teammates want a similar runtime environment
- you want easier demo setup
- you want to avoid local dependency mismatch
- you are preparing for deployment experiments
Use direct uv commands when:
- actively developing code
- running tests
- debugging backend modules
- iterating on the Streamlit app
- Start the backend:
cd backend
uv run uvicorn app.main:app --reload- Start the frontend:
cd frontend
uv run streamlit run app.py- Open the Streamlit app.
- Upload a technical manual.
- Show that the document appears in the indexed document list.
- Use the fault-code exact lookup panel for a known code such as
07orF07. - Ask a chat question:
What does fault code 07 mean and how should it be fixed?
- Show the grounded answer and source cards.
- Expand the evidence chunk.
- Ask an unsupported question:
What is the Wi-Fi password for this inverter?
- Show that the assistant refuses instead of guessing.
- Explain retrieval, citations, provenance, caching, and safety controls.
Recommended GitHub flow:
- Create or pick a GitHub issue.
- Create a branch from
dev. - Make focused changes.
- Run tests and checks.
- Commit with a clear message.
- Push the branch.
- Open a pull request into
dev. - Ask at least one teammate to review.
- Merge after tests pass and the review is approved.
- Periodically merge
devintomainfor stable demo versions.
Suggested PR checklist:
- What changed?
- Why was it needed?
- How was it tested?
- Any known limitations?
- Screenshots if UI changed?
The MVP is successful when:
- A user can upload a technical manual.
- The backend extracts and chunks the document.
- Chunks are embedded and stored in the vector database.
- The Streamlit UI can ask questions.
- The backend retrieves relevant chunks.
- The generated answer uses retrieved evidence.
- Citations and source cards are shown.
- Unsupported questions are refused in document mode.
- Exact fault-code lookup returns matching chunks without an LLM call.
- Tests pass with
uv run pytest. - The app can be started with documented uv commands.
- Table extraction from complex manuals may be imperfect.
- PDF text extraction quality depends on the PDF.
- OCR for scanned PDFs depends on local OCR tools being installed and configured.
- Exact fault-code lookup is keyword-based and may need domain-specific tuning.
- LLM quality depends on the configured provider and model.
- The project is built for local/demo use, not production multi-user deployment yet.
- Authentication and role-based access control are not implemented.
- Better table extraction.
- Advanced section heading detection.
- Metadata filters by document, equipment type, manufacturer, or model.
- Evaluation dashboard.
- Admin page for clearing indexed documents.
- User accounts and access control.
- Conversation history persistence.
- Hybrid RAG plus structured analytics over CSV or time-series datasets.
- Deployment to a cloud platform.
- Human feedback collection for answer quality.
Run:
cd backend
uv sync
uv run uvicorn app.main:app --reloadCheck .env values and confirm port 8000 is free.
Make sure the backend is running:
curl http://localhost:8000/energyIn the Streamlit sidebar, confirm Backend URL is:
http://localhost:8000
Check:
- file extension is allowed
- file is not empty
- file size is below
MAX_UPLOAD_SIZE_MB - backend logs for extraction errors
Try:
- upload a more relevant manual
- increase
TOP_K - lower
SIMILARITY_THRESHOLDcarefully - ask a more specific question
- use exact fault-code lookup for short codes
Check:
LLM_PROVIDER=openaiLLM_API_KEYis setLLM_API_BASEis correctLLM_MODELexists for that provider- rate limits have not been exceeded
Stop the backend and frontend first.
Then remove runtime data:
cd backend
rm -rf data/raw data/processed data/vector_storeRestart the backend and upload documents again.
This project is licensed under the terms in LICENSE.md.



