Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- DEPLOYMENT.md +120 -0
- Dockerfile +21 -8
- README.md +289 -112
- app.py +288 -12
- assets/.gitkeep +0 -1
- assets/rag_architecture.svg +179 -0
- eval_sample.csv +4 -0
- requirements.txt +6 -3
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DocuChat_AI Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide covers deploying DocuChat_AI as a Streamlit app on Hugging Face Spaces, with optional private access, OCR support, and evaluation workflows.
|
| 4 |
+
|
| 5 |
+
## 1. Recommended Hugging Face Setup
|
| 6 |
+
|
| 7 |
+
For the standard app:
|
| 8 |
+
|
| 9 |
+
```text
|
| 10 |
+
SDK: Streamlit
|
| 11 |
+
Python: 3.10
|
| 12 |
+
Hardware: CPU Basic or higher
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
Required files:
|
| 16 |
+
|
| 17 |
+
```text
|
| 18 |
+
app.py
|
| 19 |
+
README.md
|
| 20 |
+
requirements.txt
|
| 21 |
+
runtime.txt
|
| 22 |
+
.gitattributes
|
| 23 |
+
assets/
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
Required secret:
|
| 27 |
+
|
| 28 |
+
```text
|
| 29 |
+
GROQ_API_KEY=your_groq_key_here
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Optional private access secret:
|
| 33 |
+
|
| 34 |
+
```text
|
| 35 |
+
APP_PASSWORD=your_private_app_password
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
If `APP_PASSWORD` is set, the app shows a password screen before users can access the workspace. If it is not set, the app runs in public demo mode.
|
| 39 |
+
|
| 40 |
+
## 2. OCR Deployment Notes
|
| 41 |
+
|
| 42 |
+
DocuChat_AI includes an OCR fallback for scanned PDFs using:
|
| 43 |
+
|
| 44 |
+
```text
|
| 45 |
+
pypdfium2
|
| 46 |
+
pytesseract
|
| 47 |
+
Pillow
|
| 48 |
+
Tesseract OCR binary
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Python packages are installed from `requirements.txt`, but the Tesseract system binary may not be available in a normal Streamlit Space.
|
| 52 |
+
|
| 53 |
+
For best OCR support, deploy using the included `Dockerfile`, which installs:
|
| 54 |
+
|
| 55 |
+
```text
|
| 56 |
+
tesseract-ocr
|
| 57 |
+
poppler-utils
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Recommended OCR setup:
|
| 61 |
+
|
| 62 |
+
```text
|
| 63 |
+
SDK: Docker
|
| 64 |
+
Hardware: CPU Upgrade if processing larger scanned PDFs
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
If OCR dependencies are missing, the app fails gracefully and still works for normal text-based PDFs, DOCX, and TXT files.
|
| 68 |
+
|
| 69 |
+
## 3. Evaluation Dashboard
|
| 70 |
+
|
| 71 |
+
The Evaluation tab supports labeled RAG testing with CSV files.
|
| 72 |
+
|
| 73 |
+
Required CSV columns:
|
| 74 |
+
|
| 75 |
+
```text
|
| 76 |
+
question,expected_answer
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Optional column:
|
| 80 |
+
|
| 81 |
+
```text
|
| 82 |
+
expected_source
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Example:
|
| 86 |
+
|
| 87 |
+
```csv
|
| 88 |
+
question,expected_answer,expected_source
|
| 89 |
+
What is the contract deadline?,The deadline is 15 June 2026.,contract.pdf page 3
|
| 90 |
+
Who is the policy owner?,The policy owner is the HR department.,policy.pdf
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Metrics shown:
|
| 94 |
+
|
| 95 |
+
```text
|
| 96 |
+
Correctness score
|
| 97 |
+
Faithfulness score
|
| 98 |
+
Confidence score
|
| 99 |
+
Citation coverage
|
| 100 |
+
Retrieved chunks
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
The correctness and faithfulness scores are LLM-judged and should be treated as practical evaluation signals, not formal benchmark results.
|
| 104 |
+
|
| 105 |
+
## 4. Environment Variables
|
| 106 |
+
|
| 107 |
+
| Variable | Required | Description |
|
| 108 |
+
| --- | --- | --- |
|
| 109 |
+
| `GROQ_API_KEY` | Yes | API key for Groq LLM inference |
|
| 110 |
+
| `APP_PASSWORD` | No | Enables private password-protected app access |
|
| 111 |
+
|
| 112 |
+
## 5. Production Recommendations
|
| 113 |
+
|
| 114 |
+
- Use Hugging Face Secrets for all keys.
|
| 115 |
+
- Use Docker mode if OCR is important.
|
| 116 |
+
- Keep uploads temporary unless you add explicit user accounts and storage consent.
|
| 117 |
+
- Add `APP_PASSWORD` for portfolio demos shared with recruiters.
|
| 118 |
+
- Use the Evaluation tab with a small labeled test set before demos.
|
| 119 |
+
- Use CPU Upgrade for larger PDFs or frequent OCR use.
|
| 120 |
+
|
Dockerfile
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
|
|
| 1 |
FROM python:3.10-slim
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
ENV PIP_NO_CACHE_DIR=1
|
| 8 |
-
|
| 9 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
build-essential \
|
| 11 |
curl \
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
|
|
|
| 14 |
COPY requirements.txt .
|
| 15 |
-
RUN pip install --upgrade pip && pip install -r requirements.txt
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
COPY . .
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
EXPOSE 8501
|
| 20 |
|
| 21 |
-
|
|
|
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
| 1 |
+
# 1. Use an official lightweight Python image
|
| 2 |
FROM python:3.10-slim
|
| 3 |
|
| 4 |
+
# 2. Set the working directory inside the container
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
+
# 3. Install system dependencies for FAISS and PDF processing
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
|
|
|
|
|
|
|
|
|
| 9 |
build-essential \
|
| 10 |
curl \
|
| 11 |
+
software-properties-common \
|
| 12 |
+
git \
|
| 13 |
+
tesseract-ocr \
|
| 14 |
+
poppler-utils \
|
| 15 |
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
|
| 17 |
+
# 4. Copy only requirements first to leverage Docker caching
|
| 18 |
COPY requirements.txt .
|
|
|
|
| 19 |
|
| 20 |
+
# 5. Install Python dependencies
|
| 21 |
+
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 22 |
+
|
| 23 |
+
# 6. Copy the rest of your application code
|
| 24 |
COPY . .
|
| 25 |
|
| 26 |
+
# 7. Create the temporary directory for document uploads
|
| 27 |
+
RUN mkdir -p temp_docs && chmod 777 temp_docs
|
| 28 |
+
|
| 29 |
+
# 8. Expose the default Streamlit port
|
| 30 |
EXPOSE 8501
|
| 31 |
|
| 32 |
+
# 9. Healthcheck to ensure the app is running
|
| 33 |
+
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 34 |
|
| 35 |
+
# 10. Start the application
|
| 36 |
+
ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
title: DocuChat_AI
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: streamlit
|
|
@@ -11,181 +11,358 @@ pinned: false
|
|
| 11 |
license: mit
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
-
|
| 25 |
-
- `assets/document_processing.png`
|
| 26 |
-
- `assets/chat_with_sources.png`
|
| 27 |
|
| 28 |
-
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
| 37 |
-
- Document summarization with multiple modes
|
| 38 |
-
- Question answering over uploaded files
|
| 39 |
-
- FAISS vector search
|
| 40 |
-
- HuggingFace sentence-transformer embeddings
|
| 41 |
-
- Groq / LLM-powered generation
|
| 42 |
-
- Source citations with file, page, chunk, and preview text
|
| 43 |
-
- Chat history within the current session
|
| 44 |
-
- Export summary and chat history as Markdown
|
| 45 |
-
- Streamlit user interface
|
| 46 |
-
- Hugging Face Spaces deployment support
|
| 47 |
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
| 51 |
|
| 52 |
-
|
| 53 |
|
| 54 |
-
``
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
##
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
##
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
```bash
|
| 81 |
-
git clone
|
| 82 |
-
cd
|
| 83 |
```
|
| 84 |
|
| 85 |
-
2. Create
|
| 86 |
|
| 87 |
```bash
|
| 88 |
python -m venv .venv
|
| 89 |
-
.venv\Scripts\activate
|
| 90 |
```
|
| 91 |
|
| 92 |
-
|
| 93 |
|
| 94 |
```bash
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
| 96 |
source .venv/bin/activate
|
| 97 |
```
|
| 98 |
|
| 99 |
-
3. Install
|
| 100 |
|
| 101 |
```bash
|
| 102 |
pip install -r requirements.txt
|
| 103 |
```
|
| 104 |
|
| 105 |
-
4.
|
|
|
|
|
|
|
| 106 |
|
| 107 |
```env
|
| 108 |
-
GROQ_API_KEY=
|
| 109 |
```
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
```bash
|
| 114 |
streamlit run app.py
|
| 115 |
```
|
| 116 |
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
1. Create a new Hugging Face Space.
|
| 120 |
-
2. Choose Streamlit as the
|
| 121 |
-
3. Upload
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
|
| 128 |
-
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
| 136 |
|
| 137 |
-
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
- "What are the main recommendations?"
|
| 143 |
-
- "Explain the document for a non-technical audience."
|
| 144 |
-
- "What evidence supports this answer?"
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
| 147 |
|
| 148 |
```text
|
| 149 |
-
.
|
| 150 |
-
βββ app.py
|
| 151 |
-
βββ README.md
|
| 152 |
-
βββ requirements.txt
|
| 153 |
-
βββ runtime.txt
|
| 154 |
-
βββ Dockerfile
|
| 155 |
-
βββ .gitattributes
|
| 156 |
-
βββ assets/
|
| 157 |
-
βββ .gitkeep
|
| 158 |
```
|
| 159 |
|
| 160 |
-
|
| 161 |
|
| 162 |
-
|
| 163 |
-
- Uploaded documents are processed through temporary files and are not intentionally stored by the app.
|
| 164 |
-
- The FAISS index and chat history live only in the active Streamlit session.
|
| 165 |
-
- Users should avoid uploading highly sensitive documents to public demo deployments.
|
| 166 |
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
-
|
| 170 |
-
- Scanned PDFs may require OCR, which is not included in this version.
|
| 171 |
-
- LLM answers are grounded in retrieved chunks, but users should verify important outputs.
|
| 172 |
-
- Free Hugging Face Spaces may have CPU and memory limits.
|
| 173 |
-
- Very large documents may need smaller files or reduced chunk settings.
|
| 174 |
|
| 175 |
-
##
|
| 176 |
|
| 177 |
-
- OCR
|
| 178 |
-
-
|
| 179 |
-
-
|
| 180 |
-
-
|
| 181 |
-
-
|
| 182 |
-
|
| 183 |
-
-
|
| 184 |
|
| 185 |
-
##
|
| 186 |
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
-
##
|
| 190 |
|
| 191 |
-
|
|
|
|
| 1 |
---
|
| 2 |
title: DocuChat_AI
|
| 3 |
+
emoji: π
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: streamlit
|
|
|
|
| 11 |
license: mit
|
| 12 |
---
|
| 13 |
|
| 14 |
+
<div align="center">
|
| 15 |
|
| 16 |
+
<img src="https://capsule-render.vercel.app/api?type=waving&height=230&color=0:2563EB,100:14B8A6&text=DocuChat_AI&fontColor=ffffff&fontSize=56&fontAlignY=35&desc=Document%20Intelligence%20RAG%20Assistant&descAlignY=58&descSize=20&animation=fadeIn" />
|
| 17 |
|
| 18 |
+
<p>
|
| 19 |
+
<img src="https://readme-typing-svg.herokuapp.com?font=Fira+Code&size=17&duration=2800&pause=1000&color=2563EB¢er=true&vCenter=true&width=800&lines=Chat+with+PDF%2C+DOCX+and+TXT+documents;AI-powered+summaries+with+source-backed+answers;RAG+pipeline+using+LangChain%2C+FAISS+and+Groq;Built+with+Streamlit+for+fast+document+intelligence" alt="Typing SVG" />
|
| 20 |
+
</p>
|
| 21 |
|
| 22 |
+
<p>
|
| 23 |
+
<a href="https://huggingface.co/spaces/dineshb/DocuChat_AI">
|
| 24 |
+
<img src="https://img.shields.io/badge/Live_Demo-Hugging_Face-FFD21E?style=for-the-badge&logo=huggingface&logoColor=black" alt="Live Demo" />
|
| 25 |
+
</a>
|
| 26 |
+
<a href="https://github.com/dineshbarri/DocuChat_AI">
|
| 27 |
+
<img src="https://img.shields.io/badge/GitHub-Repository-181717?style=for-the-badge&logo=github&logoColor=white" alt="GitHub Repository" />
|
| 28 |
+
</a>
|
| 29 |
+
<a href="LICENSE">
|
| 30 |
+
<img src="https://img.shields.io/badge/License-MIT-2EA44F?style=for-the-badge" alt="MIT License" />
|
| 31 |
+
</a>
|
| 32 |
+
</p>
|
| 33 |
|
| 34 |
+
<p>
|
| 35 |
+
<img src="https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white" />
|
| 36 |
+
<img src="https://img.shields.io/badge/Streamlit-FF4B4B?style=for-the-badge&logo=streamlit&logoColor=white" />
|
| 37 |
+
<img src="https://img.shields.io/badge/LangChain-1C3C3C?style=for-the-badge" />
|
| 38 |
+
<img src="https://img.shields.io/badge/FAISS-Vector_Search-2563EB?style=for-the-badge" />
|
| 39 |
+
<img src="https://img.shields.io/badge/Groq-LLM_API-F55036?style=for-the-badge" />
|
| 40 |
+
<img src="https://img.shields.io/badge/HuggingFace-Embeddings-FFD21E?style=for-the-badge&logo=huggingface&logoColor=black" />
|
| 41 |
+
</p>
|
| 42 |
|
| 43 |
+
**DocuChat_AI is a high-end AI document chatbot that lets users upload documents, generate summaries, extract insights, and ask grounded questions with source citations.**
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
</div>
|
| 46 |
|
| 47 |
+
---
|
| 48 |
|
| 49 |
+
## π Live Application
|
| 50 |
|
| 51 |
+
Try the app here:
|
| 52 |
|
| 53 |
+
### π [Launch DocuChat_AI on Hugging Face](https://huggingface.co/spaces/dineshb/DocuChat_AI)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
DocuChat_AI is designed for students, analysts, recruiters, researchers, founders, and professionals who need to understand long documents quickly without manually reading every page.
|
| 56 |
|
| 57 |
+
---
|
| 58 |
|
| 59 |
+
## πΌοΈ Application Screenshots
|
| 60 |
|
| 61 |
+
> Add your screenshots inside the `assets/` folder using the exact filenames below. GitHub will automatically render them in this README.
|
| 62 |
+
|
| 63 |
+
<div align="center">
|
| 64 |
+
|
| 65 |
+
### 1. Premium Home Workspace
|
| 66 |
+
<img src="assets/app_home.png" width="90%" alt="DocuChat_AI home workspace" />
|
| 67 |
+
|
| 68 |
+
### 2. Document Processing and Indexing
|
| 69 |
+
<img src="assets/document_processing.png" width="90%" alt="Document upload and processing workflow" />
|
| 70 |
+
|
| 71 |
+
### 3. Chat with Source Citations
|
| 72 |
+
<img src="assets/chat_with_sources.png" width="90%" alt="Document question answering with citations" />
|
| 73 |
+
|
| 74 |
+
### 4. Summary and Analysis Tools
|
| 75 |
+
<img src="assets/summary_tools.png" width="90%" alt="Summary and analysis tabs" />
|
| 76 |
+
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
|
| 81 |
+
## π Project Overview
|
| 82 |
|
| 83 |
+
Most professionals deal with long PDFs, reports, resumes, policy documents, contracts, research papers, business notes, and technical files. Reading everything manually takes time, and generic chatbots can hallucinate when they are not grounded in the source document.
|
| 84 |
+
|
| 85 |
+
**DocuChat_AI solves this using Retrieval-Augmented Generation (RAG).**
|
| 86 |
+
|
| 87 |
+
The app extracts text from uploaded documents, splits it into searchable chunks, converts those chunks into embeddings, stores them in a FAISS vector index, retrieves the most relevant context for each question, and generates answers using an LLM while keeping responses grounded in the uploaded content.
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## β¨ Key Features
|
| 92 |
+
|
| 93 |
+
| Feature | Description |
|
| 94 |
+
| --- | --- |
|
| 95 |
+
| π Multi-document upload | Supports PDF, DOCX, and TXT files |
|
| 96 |
+
| π§ RAG-based Q&A | Ask natural-language questions about uploaded documents |
|
| 97 |
+
| π Smart summaries | Generate executive summaries, study notes, email briefs, and key takeaways |
|
| 98 |
+
| π Source citations | View retrieved document chunks used to answer each question |
|
| 99 |
+
| π§© Workflow tabs | Chat, Summaries, Extract, Analyze, and Deliverables |
|
| 100 |
+
| β‘ Quick action buttons | One-click prompts for risks, decisions, action items, FAQs, and presentation outlines |
|
| 101 |
+
| π Index stats | Track processed files, pages, chunks, and processing time |
|
| 102 |
+
| π§Ύ Document classification | LLM-based classification into Research Paper, Contract, CV, Invoice, Policy, Report, or Other |
|
| 103 |
+
| 𧬠Entity extraction | Extract people, organizations, dates, money values, and locations |
|
| 104 |
+
| β οΈ Risk detection | Surface legal risks, missing information, deadlines, and action items |
|
| 105 |
+
| π RAG quality metrics | Show retrieved chunks, confidence score, citation coverage, and context usage |
|
| 106 |
+
| π§ͺ Evaluation dashboard | Upload labeled test questions and score correctness, faithfulness, and citation coverage |
|
| 107 |
+
| π OCR-ready processing | Optional scanned PDF OCR support for Docker deployments |
|
| 108 |
+
| π Optional authentication | Protect demos with an `APP_PASSWORD` environment secret |
|
| 109 |
+
| π¬ Chat history | Maintains conversation context during the active session |
|
| 110 |
+
| β¬οΈ Export support | Export chat history as Markdown |
|
| 111 |
+
| π Secure key input | Uses Hugging Face Secrets, environment variables, or password input |
|
| 112 |
+
|
| 113 |
+
---
|
| 114 |
+
|
| 115 |
+
## π§ What Users Can Do
|
| 116 |
+
|
| 117 |
+
- Upload a long PDF and ask: **"What are the main risks in this document?"**
|
| 118 |
+
- Turn a document into an **executive summary**
|
| 119 |
+
- Extract **names, dates, metrics, definitions, and action items**
|
| 120 |
+
- Generate **meeting notes, FAQs, email briefs, and presentation outlines**
|
| 121 |
+
- Ask follow-up questions using conversation history
|
| 122 |
+
- Inspect citations to understand where the answer came from
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
|
| 126 |
+
## ποΈ RAG Architecture
|
| 127 |
|
| 128 |
+
<div align="center">
|
| 129 |
+
<img src="assets/rag_architecture.svg" width="100%" alt="DocuChat AI RAG architecture diagram" />
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
### Pipeline Flow
|
| 133 |
+
|
| 134 |
+
1. **Document Upload** - User uploads PDF, DOCX, or TXT files.
|
| 135 |
+
2. **Text Extraction** - The app extracts readable text using document loaders.
|
| 136 |
+
3. **Chunking** - Text is split into manageable chunks with overlap.
|
| 137 |
+
4. **Embedding Generation** - Chunks are converted into vector embeddings.
|
| 138 |
+
5. **Vector Search** - FAISS retrieves the most relevant chunks for each query.
|
| 139 |
+
6. **LLM Generation** - Groq-powered models generate answers using retrieved context.
|
| 140 |
+
7. **Citation Display** - The app shows source previews used for the answer.
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## π οΈ Tech Stack
|
| 145 |
+
|
| 146 |
+
| Layer | Tools |
|
| 147 |
+
| --- | --- |
|
| 148 |
+
| Frontend | Streamlit |
|
| 149 |
+
| LLM | Groq API |
|
| 150 |
+
| RAG Framework | LangChain |
|
| 151 |
+
| Vector Database | FAISS |
|
| 152 |
+
| Embeddings | Hugging Face Sentence Transformers |
|
| 153 |
+
| Document Loading | PyPDF, DOCX2TXT, LangChain loaders |
|
| 154 |
+
| Text Splitting | LangChain text splitters + tiktoken |
|
| 155 |
+
| Deployment | Hugging Face Spaces |
|
| 156 |
+
| Language | Python |
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## π Repository Structure
|
| 161 |
+
|
| 162 |
+
```text
|
| 163 |
+
DocuChat_AI/
|
| 164 |
+
β
|
| 165 |
+
βββ app.py # Main Streamlit application
|
| 166 |
+
βββ README.md # Project documentation
|
| 167 |
+
βββ requirements.txt # Python dependencies
|
| 168 |
+
βββ runtime.txt # Python runtime version for Hugging Face
|
| 169 |
+
βββ Dockerfile # Optional container deployment file
|
| 170 |
+
βββ DEPLOYMENT.md # Deployment, auth, OCR, and evaluation guide
|
| 171 |
+
βββ eval_sample.csv # Sample labeled evaluation file
|
| 172 |
+
βββ LICENSE # MIT License
|
| 173 |
+
βββ .gitignore # Local files and secrets ignored by Git
|
| 174 |
+
βββ .gitattributes # Hugging Face / Git file handling
|
| 175 |
+
β
|
| 176 |
+
βββ assets/
|
| 177 |
+
βββ app_home.png # Home UI screenshot
|
| 178 |
+
βββ document_processing.png # Upload and processing screenshot
|
| 179 |
+
βββ chat_with_sources.png # Chat answer and citations screenshot
|
| 180 |
+
βββ summary_tools.png # Summary / analysis tools screenshot
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
---
|
| 184 |
+
|
| 185 |
+
## βοΈ Local Setup
|
| 186 |
+
|
| 187 |
+
### 1. Clone the Repository
|
| 188 |
|
| 189 |
```bash
|
| 190 |
+
git clone https://github.com/dineshbarri/DocuChat_AI.git
|
| 191 |
+
cd DocuChat_AI
|
| 192 |
```
|
| 193 |
|
| 194 |
+
### 2. Create a Virtual Environment
|
| 195 |
|
| 196 |
```bash
|
| 197 |
python -m venv .venv
|
|
|
|
| 198 |
```
|
| 199 |
|
| 200 |
+
Activate it:
|
| 201 |
|
| 202 |
```bash
|
| 203 |
+
# Windows
|
| 204 |
+
.venv\Scripts\activate
|
| 205 |
+
|
| 206 |
+
# macOS / Linux
|
| 207 |
source .venv/bin/activate
|
| 208 |
```
|
| 209 |
|
| 210 |
+
### 3. Install Dependencies
|
| 211 |
|
| 212 |
```bash
|
| 213 |
pip install -r requirements.txt
|
| 214 |
```
|
| 215 |
|
| 216 |
+
### 4. Add Your Groq API Key
|
| 217 |
+
|
| 218 |
+
Create a `.env` file:
|
| 219 |
|
| 220 |
```env
|
| 221 |
+
GROQ_API_KEY=your_groq_api_key_here
|
| 222 |
```
|
| 223 |
|
| 224 |
+
You can create a Groq API key from:
|
| 225 |
+
|
| 226 |
+
π [https://console.groq.com/](https://console.groq.com/)
|
| 227 |
+
|
| 228 |
+
### 5. Run the App
|
| 229 |
|
| 230 |
```bash
|
| 231 |
streamlit run app.py
|
| 232 |
```
|
| 233 |
|
| 234 |
+
Open the local URL shown in your terminal.
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## π How to Get a Groq API Key
|
| 239 |
+
|
| 240 |
+
1. Go to [Groq Console](https://console.groq.com/)
|
| 241 |
+
2. Sign in or create an account
|
| 242 |
+
3. Open **API Keys**
|
| 243 |
+
4. Create a new API key
|
| 244 |
+
5. Copy the key
|
| 245 |
+
6. Paste it into the app sidebar or save it in `.env`
|
| 246 |
+
|
| 247 |
+
For Hugging Face Spaces, add it as a secret:
|
| 248 |
+
|
| 249 |
+
```text
|
| 250 |
+
GROQ_API_KEY = your_key_here
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
---
|
| 254 |
+
|
| 255 |
+
## π’ Hugging Face Deployment
|
| 256 |
|
| 257 |
1. Create a new Hugging Face Space.
|
| 258 |
+
2. Choose **Streamlit** as the SDK.
|
| 259 |
+
3. Upload:
|
| 260 |
+
- `app.py`
|
| 261 |
+
- `README.md`
|
| 262 |
+
- `requirements.txt`
|
| 263 |
+
- `runtime.txt`
|
| 264 |
+
- `.gitattributes`
|
| 265 |
+
- `assets/`
|
| 266 |
+
4. Add `GROQ_API_KEY` in **Settings -> Secrets**.
|
| 267 |
+
5. Optional: add `APP_PASSWORD` in **Settings -> Secrets** to protect the app.
|
| 268 |
+
6. Restart the Space after dependency changes.
|
| 269 |
+
|
| 270 |
+
For scanned PDF OCR, use the included Dockerfile because OCR needs system packages such as Tesseract.
|
| 271 |
+
|
| 272 |
+
See [DEPLOYMENT.md](DEPLOYMENT.md) for the full deployment guide.
|
| 273 |
+
|
| 274 |
+
---
|
| 275 |
|
| 276 |
+
## π‘ Example Questions
|
| 277 |
|
| 278 |
+
Try these after uploading and processing a document:
|
| 279 |
|
| 280 |
+
```text
|
| 281 |
+
Summarize this document in 6 crisp bullet points.
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
```text
|
| 285 |
+
What are the key risks, warnings, or limitations?
|
| 286 |
+
```
|
| 287 |
|
| 288 |
+
```text
|
| 289 |
+
Extract all names, dates, numbers, and important terms.
|
| 290 |
+
```
|
| 291 |
|
| 292 |
+
```text
|
| 293 |
+
Create an action-item checklist from this document.
|
| 294 |
+
```
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
+
```text
|
| 297 |
+
Explain this document like I am new to the topic.
|
| 298 |
+
```
|
| 299 |
|
| 300 |
```text
|
| 301 |
+
Create a presentation outline from this document.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
```
|
| 303 |
|
| 304 |
+
---
|
| 305 |
|
| 306 |
+
## π Privacy and Security
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
+
- Uploaded files are processed through temporary files.
|
| 309 |
+
- The app does not intentionally store user documents.
|
| 310 |
+
- The FAISS index and chat history live inside the active Streamlit session.
|
| 311 |
+
- API keys should be stored in environment variables or Hugging Face Secrets.
|
| 312 |
+
- Avoid uploading highly confidential documents to public demo environments.
|
| 313 |
|
| 314 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
+
## β οΈ Limitations
|
| 317 |
|
| 318 |
+
- Scanned PDF OCR requires optional OCR dependencies and works best in Docker mode.
|
| 319 |
+
- Very large documents may be limited by CPU and memory on free hosting.
|
| 320 |
+
- Complex tables may not extract perfectly from PDFs.
|
| 321 |
+
- LLM answers should be reviewed for critical legal, medical, financial, or compliance use cases.
|
| 322 |
+
- Citation quality depends on document extraction and retrieval quality.
|
| 323 |
+
|
| 324 |
+
---
|
| 325 |
|
| 326 |
+
## π Future Improvements
|
| 327 |
|
| 328 |
+
- Multi-document comparison mode
|
| 329 |
+
- Persistent user workspaces
|
| 330 |
+
- Better page-level citation highlighting
|
| 331 |
+
- CSV, Excel, PPTX, and web page support
|
| 332 |
+
- Reranking for improved retrieval accuracy
|
| 333 |
+
- Authentication and private document libraries
|
| 334 |
+
- Downloadable summary reports
|
| 335 |
+
- Evaluation dashboard for retrieval quality
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
## π§βπ» Author
|
| 340 |
+
|
| 341 |
+
<div align="center">
|
| 342 |
+
|
| 343 |
+
### Built by **Dinesh Barri**
|
| 344 |
+
|
| 345 |
+
Data Analyst | AI Automation Engineer | Founder @ Plemdo AI
|
| 346 |
+
|
| 347 |
+
<p>
|
| 348 |
+
<a href="https://dineshbarri.dev">
|
| 349 |
+
<img src="https://img.shields.io/badge/Portfolio-dineshbarri.dev-FF5722?style=for-the-badge&logo=vercel&logoColor=white" />
|
| 350 |
+
</a>
|
| 351 |
+
<a href="https://github.com/dineshbarri">
|
| 352 |
+
<img src="https://img.shields.io/badge/GitHub-dineshbarri-181717?style=for-the-badge&logo=github&logoColor=white" />
|
| 353 |
+
</a>
|
| 354 |
+
<a href="https://www.linkedin.com/in/dinesh-barri-7654b010b/">
|
| 355 |
+
<img src="https://img.shields.io/badge/LinkedIn-Dinesh%20Barri-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" />
|
| 356 |
+
</a>
|
| 357 |
+
<a href="mailto:dineshbarri1997@gmail.com">
|
| 358 |
+
<img src="https://img.shields.io/badge/Email-dineshbarri1997%40gmail.com-D14836?style=for-the-badge&logo=gmail&logoColor=white" />
|
| 359 |
+
</a>
|
| 360 |
+
</p>
|
| 361 |
+
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
---
|
| 365 |
|
| 366 |
+
## β Support
|
| 367 |
|
| 368 |
+
If you found this project useful, consider giving it a star on GitHub. It helps the project reach more developers, recruiters, and AI builders.
|
app.py
CHANGED
|
@@ -3,6 +3,8 @@ import time
|
|
| 3 |
import json
|
| 4 |
import hashlib
|
| 5 |
import tempfile
|
|
|
|
|
|
|
| 6 |
import streamlit as st
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
from datetime import datetime
|
|
@@ -15,6 +17,7 @@ from langchain_community.vectorstores import FAISS
|
|
| 15 |
from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader
|
| 16 |
from langchain_huggingface import HuggingFaceEmbeddings
|
| 17 |
from langchain_core.messages import HumanMessage, AIMessage
|
|
|
|
| 18 |
from langchain.chains import create_history_aware_retriever
|
| 19 |
from langchain_core.output_parsers import StrOutputParser
|
| 20 |
|
|
@@ -23,7 +26,7 @@ from langchain_core.output_parsers import StrOutputParser
|
|
| 23 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
load_dotenv()
|
| 25 |
st.set_page_config(
|
| 26 |
-
page_title="
|
| 27 |
page_icon="π",
|
| 28 |
layout="wide",
|
| 29 |
initial_sidebar_state="expanded"
|
|
@@ -290,6 +293,9 @@ for key, default in {
|
|
| 290 |
"doc_stats": {},
|
| 291 |
"doc_intelligence": {},
|
| 292 |
"rag_metrics": {},
|
|
|
|
|
|
|
|
|
|
| 293 |
"last_file_hash": "",
|
| 294 |
"full_raw_text": "", # BUG FIX: Stores text so summary can use it without reloading
|
| 295 |
"pending_query": "",
|
|
@@ -336,7 +342,62 @@ def compute_files_hash(files) -> str:
|
|
| 336 |
h.update(str(f.size).encode())
|
| 337 |
return h.hexdigest()
|
| 338 |
|
| 339 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
"""Safely load documents using tempfile (No local folder clutter)."""
|
| 341 |
docs = []
|
| 342 |
for file in files:
|
|
@@ -354,6 +415,12 @@ def load_documents(files) -> list:
|
|
| 354 |
loader = TextLoader(temp_path, encoding="utf-8")
|
| 355 |
|
| 356 |
loaded = loader.load()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
docs.extend(loaded[:MAX_PAGES])
|
| 358 |
except Exception as e:
|
| 359 |
st.error(f"β οΈ Error loading `{file.name}`: {e}")
|
|
@@ -363,7 +430,7 @@ def load_documents(files) -> list:
|
|
| 363 |
return docs
|
| 364 |
|
| 365 |
def export_chat() -> str:
|
| 366 |
-
lines = [f"#
|
| 367 |
for m in st.session_state.messages:
|
| 368 |
role = "π€ User" if m["role"] == "user" else "π€ Assistant"
|
| 369 |
lines.append(f"**{role}:** {m['content']}\n")
|
|
@@ -483,6 +550,148 @@ def calculate_rag_metrics(retrieved_docs, top_k: int, context_chars: int, answer
|
|
| 483 |
"generated_at": datetime.now().strftime("%H:%M:%S"),
|
| 484 |
}
|
| 485 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
def queue_prompt(prompt: str):
|
| 487 |
st.session_state.pending_query = prompt
|
| 488 |
|
|
@@ -496,7 +705,7 @@ def render_hero():
|
|
| 496 |
<div class="hero-layout">
|
| 497 |
<div>
|
| 498 |
<div class="hero-kicker">Document intelligence workspace</div>
|
| 499 |
-
<h1>
|
| 500 |
<p>Upload documents, generate summaries, extract insights, and ask grounded questions with source citations.</p>
|
| 501 |
</div>
|
| 502 |
<aside class="signature-card">
|
|
@@ -537,9 +746,9 @@ def render_capability_cards():
|
|
| 537 |
"""
|
| 538 |
<div class="feature-grid">
|
| 539 |
<div class="feature-card"><strong>Ask Anything</strong><span>Chat with PDFs, Word files, and text documents using grounded answers.</span></div>
|
| 540 |
-
<div class="feature-card"><strong>
|
| 541 |
-
<div class="feature-card"><strong>
|
| 542 |
-
<div class="feature-card"><strong>
|
| 543 |
</div>
|
| 544 |
""",
|
| 545 |
unsafe_allow_html=True,
|
|
@@ -627,6 +836,60 @@ def render_intelligence_panel():
|
|
| 627 |
q4.metric("Context Used", f"{metrics.get('context_utilization', 0)}%")
|
| 628 |
st.caption(f"Last evaluated at {metrics.get('generated_at', 'N/A')}. These are lightweight heuristic metrics for visibility, not formal benchmark scores.")
|
| 629 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
def render_workspace():
|
| 631 |
render_hero()
|
| 632 |
render_status_panel()
|
|
@@ -635,12 +898,13 @@ def render_workspace():
|
|
| 635 |
st.markdown('<div class="section-title">Document command center</div>', unsafe_allow_html=True)
|
| 636 |
st.markdown('<p class="section-copy">Choose a workflow or start with a suggested prompt. Each button sends a ready-made instruction to the assistant.</p>', unsafe_allow_html=True)
|
| 637 |
|
| 638 |
-
chat_tab, summary_tab, extract_tab, analyze_tab, intel_tab, deliver_tab = st.tabs([
|
| 639 |
"Chat",
|
| 640 |
"Summaries",
|
| 641 |
"Extract",
|
| 642 |
"Analyze",
|
| 643 |
"Intelligence",
|
|
|
|
| 644 |
"Deliverables",
|
| 645 |
])
|
| 646 |
|
|
@@ -681,6 +945,10 @@ def render_workspace():
|
|
| 681 |
st.markdown("#### AI document intelligence")
|
| 682 |
render_intelligence_panel()
|
| 683 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
with deliver_tab:
|
| 685 |
st.markdown("#### Ready-to-use outputs")
|
| 686 |
c1, c2, c3 = st.columns(3)
|
|
@@ -694,8 +962,14 @@ def render_workspace():
|
|
| 694 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 695 |
# SIDEBAR UI
|
| 696 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 697 |
with st.sidebar:
|
| 698 |
-
st.title("π
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
|
| 700 |
if st.session_state.vectors:
|
| 701 |
st.success("β
Vector DB Ready", icon="π’")
|
|
@@ -715,6 +989,8 @@ with st.sidebar:
|
|
| 715 |
with st.expander("βοΈ Advanced Settings"):
|
| 716 |
temperature = st.slider("Temperature", 0.0, 1.0, 0.3, 0.05)
|
| 717 |
top_k = st.slider("Retrieved Chunks (Top-K)", 2, 10, 4)
|
|
|
|
|
|
|
| 718 |
|
| 719 |
st.divider()
|
| 720 |
|
|
@@ -736,7 +1012,7 @@ with st.sidebar:
|
|
| 736 |
elif not uploaded_files:
|
| 737 |
st.warning("β οΈ Upload files first.")
|
| 738 |
else:
|
| 739 |
-
file_hash = compute_files_hash(uploaded_files)
|
| 740 |
force_reprocess = (file_hash != st.session_state.last_file_hash)
|
| 741 |
|
| 742 |
if not force_reprocess and st.session_state.vectors:
|
|
@@ -747,7 +1023,7 @@ with st.sidebar:
|
|
| 747 |
t0 = time.time()
|
| 748 |
|
| 749 |
st.write("π₯ Loading files into memory...")
|
| 750 |
-
raw_docs = load_documents(uploaded_files)
|
| 751 |
|
| 752 |
if not raw_docs:
|
| 753 |
status.update(label="No content found!", state="error")
|
|
@@ -799,7 +1075,7 @@ with st.sidebar:
|
|
| 799 |
|
| 800 |
# BUG FIX: Use the saved text instead of raw_docs
|
| 801 |
if not st.session_state.full_raw_text:
|
| 802 |
-
temp_docs = load_documents(uploaded_files)
|
| 803 |
st.session_state.full_raw_text = " ".join([d.page_content for d in temp_docs])
|
| 804 |
|
| 805 |
full_text = st.session_state.full_raw_text[:6000]
|
|
|
|
| 3 |
import json
|
| 4 |
import hashlib
|
| 5 |
import tempfile
|
| 6 |
+
import csv
|
| 7 |
+
import io
|
| 8 |
import streamlit as st
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
from datetime import datetime
|
|
|
|
| 17 |
from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader
|
| 18 |
from langchain_huggingface import HuggingFaceEmbeddings
|
| 19 |
from langchain_core.messages import HumanMessage, AIMessage
|
| 20 |
+
from langchain_core.documents import Document
|
| 21 |
from langchain.chains import create_history_aware_retriever
|
| 22 |
from langchain_core.output_parsers import StrOutputParser
|
| 23 |
|
|
|
|
| 26 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
load_dotenv()
|
| 28 |
st.set_page_config(
|
| 29 |
+
page_title="DocuChat_AT",
|
| 30 |
page_icon="π",
|
| 31 |
layout="wide",
|
| 32 |
initial_sidebar_state="expanded"
|
|
|
|
| 293 |
"doc_stats": {},
|
| 294 |
"doc_intelligence": {},
|
| 295 |
"rag_metrics": {},
|
| 296 |
+
"eval_results": [],
|
| 297 |
+
"eval_summary": {},
|
| 298 |
+
"auth_ok": False,
|
| 299 |
"last_file_hash": "",
|
| 300 |
"full_raw_text": "", # BUG FIX: Stores text so summary can use it without reloading
|
| 301 |
"pending_query": "",
|
|
|
|
| 342 |
h.update(str(f.size).encode())
|
| 343 |
return h.hexdigest()
|
| 344 |
|
| 345 |
+
def require_authentication():
|
| 346 |
+
app_password = os.getenv("APP_PASSWORD", "").strip()
|
| 347 |
+
if not app_password:
|
| 348 |
+
st.session_state.auth_ok = True
|
| 349 |
+
return
|
| 350 |
+
if st.session_state.auth_ok:
|
| 351 |
+
return
|
| 352 |
+
|
| 353 |
+
st.markdown("### π Private Workspace")
|
| 354 |
+
st.caption("This deployment is protected. Enter the app password to continue.")
|
| 355 |
+
password = st.text_input("App Password", type="password", placeholder="Enter workspace password")
|
| 356 |
+
if st.button("Unlock Workspace", type="primary"):
|
| 357 |
+
if password == app_password:
|
| 358 |
+
st.session_state.auth_ok = True
|
| 359 |
+
st.rerun()
|
| 360 |
+
else:
|
| 361 |
+
st.error("Incorrect password.")
|
| 362 |
+
st.stop()
|
| 363 |
+
|
| 364 |
+
def ocr_pdf_pages(pdf_path: str, source_name: str, max_pages: int = 8) -> list:
|
| 365 |
+
"""Optional OCR for scanned PDFs. Requires pypdfium2, pytesseract, Pillow, and Tesseract binary."""
|
| 366 |
+
try:
|
| 367 |
+
import pypdfium2 as pdfium
|
| 368 |
+
import pytesseract
|
| 369 |
+
except Exception as e:
|
| 370 |
+
raise RuntimeError("OCR packages are not installed. Install pypdfium2, pytesseract, and Pillow.") from e
|
| 371 |
+
|
| 372 |
+
docs = []
|
| 373 |
+
pdf = pdfium.PdfDocument(pdf_path)
|
| 374 |
+
page_count = min(len(pdf), max_pages)
|
| 375 |
+
for page_index in range(page_count):
|
| 376 |
+
page = pdf[page_index]
|
| 377 |
+
bitmap = page.render(scale=2.0)
|
| 378 |
+
image = bitmap.to_pil()
|
| 379 |
+
text = pytesseract.image_to_string(image)
|
| 380 |
+
if text.strip():
|
| 381 |
+
docs.append(
|
| 382 |
+
Document(
|
| 383 |
+
page_content=text,
|
| 384 |
+
metadata={
|
| 385 |
+
"source": source_name,
|
| 386 |
+
"page": page_index,
|
| 387 |
+
"extraction": "ocr",
|
| 388 |
+
},
|
| 389 |
+
)
|
| 390 |
+
)
|
| 391 |
+
return docs
|
| 392 |
+
|
| 393 |
+
def normalize_source_metadata(docs: list, source_name: str, file_type: str, extraction: str = "text") -> list:
|
| 394 |
+
for doc in docs:
|
| 395 |
+
doc.metadata["source"] = source_name
|
| 396 |
+
doc.metadata["file_type"] = file_type
|
| 397 |
+
doc.metadata.setdefault("extraction", extraction)
|
| 398 |
+
return docs
|
| 399 |
+
|
| 400 |
+
def load_documents(files, use_ocr: bool = False, ocr_page_limit: int = 8) -> list:
|
| 401 |
"""Safely load documents using tempfile (No local folder clutter)."""
|
| 402 |
docs = []
|
| 403 |
for file in files:
|
|
|
|
| 415 |
loader = TextLoader(temp_path, encoding="utf-8")
|
| 416 |
|
| 417 |
loaded = loader.load()
|
| 418 |
+
loaded = normalize_source_metadata(loaded, file.name, ext, "text")
|
| 419 |
+
if ext == ".pdf" and use_ocr:
|
| 420 |
+
extracted_chars = sum(len(doc.page_content.strip()) for doc in loaded)
|
| 421 |
+
if extracted_chars < 250:
|
| 422 |
+
st.write(f"π Running OCR for scanned PDF: {file.name}")
|
| 423 |
+
loaded = ocr_pdf_pages(temp_path, file.name, max_pages=ocr_page_limit)
|
| 424 |
docs.extend(loaded[:MAX_PAGES])
|
| 425 |
except Exception as e:
|
| 426 |
st.error(f"β οΈ Error loading `{file.name}`: {e}")
|
|
|
|
| 430 |
return docs
|
| 431 |
|
| 432 |
def export_chat() -> str:
|
| 433 |
+
lines = [f"# DocuChat_AT Export β {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"]
|
| 434 |
for m in st.session_state.messages:
|
| 435 |
role = "π€ User" if m["role"] == "user" else "π€ Assistant"
|
| 436 |
lines.append(f"**{role}:** {m['content']}\n")
|
|
|
|
| 550 |
"generated_at": datetime.now().strftime("%H:%M:%S"),
|
| 551 |
}
|
| 552 |
|
| 553 |
+
def build_retriever(llm, top_k: int):
|
| 554 |
+
retriever = st.session_state.vectors.as_retriever(
|
| 555 |
+
search_type="mmr",
|
| 556 |
+
search_kwargs={"k": top_k, "fetch_k": top_k * 3},
|
| 557 |
+
)
|
| 558 |
+
ctx_prompt = ChatPromptTemplate.from_messages([
|
| 559 |
+
("system", "Given the chat history and the latest user question, rephrase it as a standalone search query. Return ONLY the reformulated query."),
|
| 560 |
+
MessagesPlaceholder("chat_history"),
|
| 561 |
+
("human", "{input}"),
|
| 562 |
+
])
|
| 563 |
+
return create_history_aware_retriever(llm, retriever, ctx_prompt)
|
| 564 |
+
|
| 565 |
+
def format_retrieved_context(retrieved_docs: list) -> tuple[str, int]:
|
| 566 |
+
context_parts = []
|
| 567 |
+
total_chars = 0
|
| 568 |
+
for doc in retrieved_docs:
|
| 569 |
+
if total_chars + len(doc.page_content) <= MAX_CONTEXT_CHARS:
|
| 570 |
+
context_parts.append(doc.page_content)
|
| 571 |
+
total_chars += len(doc.page_content)
|
| 572 |
+
else:
|
| 573 |
+
remaining = MAX_CONTEXT_CHARS - total_chars
|
| 574 |
+
if remaining > 200:
|
| 575 |
+
context_parts.append(doc.page_content[:remaining])
|
| 576 |
+
total_chars += remaining
|
| 577 |
+
break
|
| 578 |
+
return "\n\n---\n\n".join(context_parts), total_chars
|
| 579 |
+
|
| 580 |
+
def answer_from_documents(llm, user_query: str, top_k: int, chat_history=None) -> dict:
|
| 581 |
+
history = chat_history if chat_history is not None else st.session_state.chat_history
|
| 582 |
+
history_aware_retriever = build_retriever(llm, top_k)
|
| 583 |
+
retrieved_docs = history_aware_retriever.invoke({
|
| 584 |
+
"input": user_query,
|
| 585 |
+
"chat_history": history,
|
| 586 |
+
})
|
| 587 |
+
formatted_context, total_chars = format_retrieved_context(retrieved_docs)
|
| 588 |
+
qa_prompt = ChatPromptTemplate.from_messages([
|
| 589 |
+
("system", "You are an expert assistant. Answer using ONLY the provided context. If the answer isn't in the context, say so clearly.\n\nContext:\n{context}"),
|
| 590 |
+
MessagesPlaceholder("chat_history"),
|
| 591 |
+
("human", "{input}"),
|
| 592 |
+
])
|
| 593 |
+
qa_chain = qa_prompt | llm | StrOutputParser()
|
| 594 |
+
answer = qa_chain.invoke({
|
| 595 |
+
"input": user_query,
|
| 596 |
+
"chat_history": history,
|
| 597 |
+
"context": formatted_context,
|
| 598 |
+
})
|
| 599 |
+
return {
|
| 600 |
+
"answer": answer,
|
| 601 |
+
"retrieved_docs": retrieved_docs,
|
| 602 |
+
"context": formatted_context,
|
| 603 |
+
"context_chars": total_chars,
|
| 604 |
+
"metrics": calculate_rag_metrics(retrieved_docs, top_k, total_chars, answer),
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
def parse_eval_csv(uploaded_file) -> list:
|
| 608 |
+
raw = uploaded_file.getvalue().decode("utf-8-sig")
|
| 609 |
+
reader = csv.DictReader(io.StringIO(raw))
|
| 610 |
+
rows = []
|
| 611 |
+
for row in reader:
|
| 612 |
+
question = (row.get("question") or row.get("Question") or "").strip()
|
| 613 |
+
expected = (row.get("expected_answer") or row.get("Expected Answer") or row.get("answer") or "").strip()
|
| 614 |
+
expected_source = (row.get("expected_source") or row.get("source") or "").strip()
|
| 615 |
+
if question and expected:
|
| 616 |
+
rows.append({
|
| 617 |
+
"question": question,
|
| 618 |
+
"expected_answer": expected,
|
| 619 |
+
"expected_source": expected_source,
|
| 620 |
+
})
|
| 621 |
+
return rows
|
| 622 |
+
|
| 623 |
+
def judge_eval_answer(llm, question: str, expected_answer: str, actual_answer: str, context: str) -> dict:
|
| 624 |
+
judge_prompt = ChatPromptTemplate.from_template(
|
| 625 |
+
"""
|
| 626 |
+
You are evaluating a RAG answer. Return ONLY valid JSON.
|
| 627 |
+
|
| 628 |
+
Score:
|
| 629 |
+
- correctness_score: 0-100, how well the actual answer matches the expected answer.
|
| 630 |
+
- faithfulness_score: 0-100, whether the actual answer is supported by the retrieved context.
|
| 631 |
+
- notes: one short sentence.
|
| 632 |
+
|
| 633 |
+
JSON schema:
|
| 634 |
+
{{
|
| 635 |
+
"correctness_score": 0-100,
|
| 636 |
+
"faithfulness_score": 0-100,
|
| 637 |
+
"notes": "short note"
|
| 638 |
+
}}
|
| 639 |
+
|
| 640 |
+
Question: {question}
|
| 641 |
+
Expected answer: {expected_answer}
|
| 642 |
+
Actual answer: {actual_answer}
|
| 643 |
+
Retrieved context: {context}
|
| 644 |
+
"""
|
| 645 |
+
)
|
| 646 |
+
parsed = safe_json_loads((judge_prompt | llm | StrOutputParser()).invoke({
|
| 647 |
+
"question": question,
|
| 648 |
+
"expected_answer": expected_answer,
|
| 649 |
+
"actual_answer": actual_answer,
|
| 650 |
+
"context": context[:6000],
|
| 651 |
+
}))
|
| 652 |
+
return {
|
| 653 |
+
"correctness_score": int(parsed.get("correctness_score", 0) or 0),
|
| 654 |
+
"faithfulness_score": int(parsed.get("faithfulness_score", 0) or 0),
|
| 655 |
+
"notes": parsed.get("notes", "Evaluation completed."),
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
def run_eval_suite(eval_rows: list, llm, top_k: int) -> tuple[list, dict]:
|
| 659 |
+
results = []
|
| 660 |
+
for index, row in enumerate(eval_rows, start=1):
|
| 661 |
+
rag = answer_from_documents(llm, row["question"], top_k, chat_history=[])
|
| 662 |
+
judge = judge_eval_answer(
|
| 663 |
+
llm,
|
| 664 |
+
row["question"],
|
| 665 |
+
row["expected_answer"],
|
| 666 |
+
rag["answer"],
|
| 667 |
+
rag["context"],
|
| 668 |
+
)
|
| 669 |
+
results.append({
|
| 670 |
+
"test_id": index,
|
| 671 |
+
"question": row["question"],
|
| 672 |
+
"expected_answer": row["expected_answer"],
|
| 673 |
+
"actual_answer": rag["answer"],
|
| 674 |
+
"retrieved_chunks": rag["metrics"]["retrieved_chunks"],
|
| 675 |
+
"citation_coverage": rag["metrics"]["citation_coverage"],
|
| 676 |
+
"confidence_score": rag["metrics"]["confidence_score"],
|
| 677 |
+
"correctness_score": judge["correctness_score"],
|
| 678 |
+
"faithfulness_score": judge["faithfulness_score"],
|
| 679 |
+
"notes": judge["notes"],
|
| 680 |
+
})
|
| 681 |
+
|
| 682 |
+
if not results:
|
| 683 |
+
return [], {}
|
| 684 |
+
|
| 685 |
+
summary = {
|
| 686 |
+
"tests": len(results),
|
| 687 |
+
"avg_correctness": round(sum(r["correctness_score"] for r in results) / len(results), 1),
|
| 688 |
+
"avg_faithfulness": round(sum(r["faithfulness_score"] for r in results) / len(results), 1),
|
| 689 |
+
"avg_confidence": round(sum(r["confidence_score"] for r in results) / len(results), 1),
|
| 690 |
+
"avg_citation_coverage": round(sum(r["citation_coverage"] for r in results) / len(results), 1),
|
| 691 |
+
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 692 |
+
}
|
| 693 |
+
return results, summary
|
| 694 |
+
|
| 695 |
def queue_prompt(prompt: str):
|
| 696 |
st.session_state.pending_query = prompt
|
| 697 |
|
|
|
|
| 705 |
<div class="hero-layout">
|
| 706 |
<div>
|
| 707 |
<div class="hero-kicker">Document intelligence workspace</div>
|
| 708 |
+
<h1>DocuChat_AT (Document Intelligence RAG Assistant)</h1>
|
| 709 |
<p>Upload documents, generate summaries, extract insights, and ask grounded questions with source citations.</p>
|
| 710 |
</div>
|
| 711 |
<aside class="signature-card">
|
|
|
|
| 746 |
"""
|
| 747 |
<div class="feature-grid">
|
| 748 |
<div class="feature-card"><strong>Ask Anything</strong><span>Chat with PDFs, Word files, and text documents using grounded answers.</span></div>
|
| 749 |
+
<div class="feature-card"><strong>AI Intelligence</strong><span>Classify documents, extract entities, detect risks, and identify actions.</span></div>
|
| 750 |
+
<div class="feature-card"><strong>Eval Dashboard</strong><span>Run labeled test questions and score correctness, faithfulness, and citations.</span></div>
|
| 751 |
+
<div class="feature-card"><strong>OCR Ready</strong><span>Optional scanned PDF OCR with graceful fallback for Streamlit deployments.</span></div>
|
| 752 |
</div>
|
| 753 |
""",
|
| 754 |
unsafe_allow_html=True,
|
|
|
|
| 836 |
q4.metric("Context Used", f"{metrics.get('context_utilization', 0)}%")
|
| 837 |
st.caption(f"Last evaluated at {metrics.get('generated_at', 'N/A')}. These are lightweight heuristic metrics for visibility, not formal benchmark scores.")
|
| 838 |
|
| 839 |
+
def render_evaluation_panel():
|
| 840 |
+
if not st.session_state.vectors:
|
| 841 |
+
st.info("Process documents before running an evaluation suite.")
|
| 842 |
+
return
|
| 843 |
+
|
| 844 |
+
st.markdown("##### Upload labeled test questions")
|
| 845 |
+
st.caption("CSV columns required: question, expected_answer. Optional: expected_source.")
|
| 846 |
+
eval_file = st.file_uploader("Evaluation CSV", type=["csv"], key="eval_csv")
|
| 847 |
+
|
| 848 |
+
c1, c2 = st.columns([1, 1])
|
| 849 |
+
with c1:
|
| 850 |
+
run_eval = st.button("π§ͺ Run Evaluation", type="primary", use_container_width=True)
|
| 851 |
+
with c2:
|
| 852 |
+
clear_eval = st.button("Clear Evaluation", use_container_width=True)
|
| 853 |
+
|
| 854 |
+
if clear_eval:
|
| 855 |
+
st.session_state.eval_results = []
|
| 856 |
+
st.session_state.eval_summary = {}
|
| 857 |
+
st.rerun()
|
| 858 |
+
|
| 859 |
+
if run_eval:
|
| 860 |
+
if not eval_file:
|
| 861 |
+
st.warning("Upload a labeled CSV first.")
|
| 862 |
+
else:
|
| 863 |
+
rows = parse_eval_csv(eval_file)
|
| 864 |
+
if not rows:
|
| 865 |
+
st.error("No valid rows found. Use columns: question, expected_answer.")
|
| 866 |
+
else:
|
| 867 |
+
with st.spinner(f"Running {len(rows)} RAG evaluation tests..."):
|
| 868 |
+
st.session_state.eval_results, st.session_state.eval_summary = run_eval_suite(rows, llm, top_k)
|
| 869 |
+
st.success("Evaluation complete.")
|
| 870 |
+
|
| 871 |
+
summary = st.session_state.get("eval_summary", {})
|
| 872 |
+
results = st.session_state.get("eval_results", [])
|
| 873 |
+
if summary:
|
| 874 |
+
m1, m2, m3, m4 = st.columns(4)
|
| 875 |
+
m1.metric("Tests", summary.get("tests", 0))
|
| 876 |
+
m2.metric("Correctness", f"{summary.get('avg_correctness', 0)}%")
|
| 877 |
+
m3.metric("Faithfulness", f"{summary.get('avg_faithfulness', 0)}%")
|
| 878 |
+
m4.metric("Citation Coverage", f"{summary.get('avg_citation_coverage', 0)}%")
|
| 879 |
+
st.caption(f"Generated at {summary.get('generated_at')}. Scores are LLM-judged and should be reviewed for critical use cases.")
|
| 880 |
+
|
| 881 |
+
if results:
|
| 882 |
+
st.markdown("##### Test Results")
|
| 883 |
+
st.dataframe(results, use_container_width=True, hide_index=True)
|
| 884 |
+
export = json.dumps({"summary": summary, "results": results}, indent=2)
|
| 885 |
+
st.download_button(
|
| 886 |
+
"β¬οΈ Download Evaluation JSON",
|
| 887 |
+
data=export,
|
| 888 |
+
file_name=f"docuchat_eval_{datetime.now().strftime('%Y%m%d_%H%M')}.json",
|
| 889 |
+
mime="application/json",
|
| 890 |
+
use_container_width=True,
|
| 891 |
+
)
|
| 892 |
+
|
| 893 |
def render_workspace():
|
| 894 |
render_hero()
|
| 895 |
render_status_panel()
|
|
|
|
| 898 |
st.markdown('<div class="section-title">Document command center</div>', unsafe_allow_html=True)
|
| 899 |
st.markdown('<p class="section-copy">Choose a workflow or start with a suggested prompt. Each button sends a ready-made instruction to the assistant.</p>', unsafe_allow_html=True)
|
| 900 |
|
| 901 |
+
chat_tab, summary_tab, extract_tab, analyze_tab, intel_tab, eval_tab, deliver_tab = st.tabs([
|
| 902 |
"Chat",
|
| 903 |
"Summaries",
|
| 904 |
"Extract",
|
| 905 |
"Analyze",
|
| 906 |
"Intelligence",
|
| 907 |
+
"Evaluation",
|
| 908 |
"Deliverables",
|
| 909 |
])
|
| 910 |
|
|
|
|
| 945 |
st.markdown("#### AI document intelligence")
|
| 946 |
render_intelligence_panel()
|
| 947 |
|
| 948 |
+
with eval_tab:
|
| 949 |
+
st.markdown("#### RAG evaluation dashboard")
|
| 950 |
+
render_evaluation_panel()
|
| 951 |
+
|
| 952 |
with deliver_tab:
|
| 953 |
st.markdown("#### Ready-to-use outputs")
|
| 954 |
c1, c2, c3 = st.columns(3)
|
|
|
|
| 962 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 963 |
# SIDEBAR UI
|
| 964 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 965 |
+
require_authentication()
|
| 966 |
+
|
| 967 |
with st.sidebar:
|
| 968 |
+
st.title("π DocuChat_AT")
|
| 969 |
+
if os.getenv("APP_PASSWORD", "").strip():
|
| 970 |
+
st.success("π Private mode enabled")
|
| 971 |
+
else:
|
| 972 |
+
st.caption("Public demo mode")
|
| 973 |
|
| 974 |
if st.session_state.vectors:
|
| 975 |
st.success("β
Vector DB Ready", icon="π’")
|
|
|
|
| 989 |
with st.expander("βοΈ Advanced Settings"):
|
| 990 |
temperature = st.slider("Temperature", 0.0, 1.0, 0.3, 0.05)
|
| 991 |
top_k = st.slider("Retrieved Chunks (Top-K)", 2, 10, 4)
|
| 992 |
+
use_ocr = st.checkbox("Enable OCR fallback for scanned PDFs", value=False)
|
| 993 |
+
ocr_page_limit = st.slider("OCR Page Limit", 1, 25, 8)
|
| 994 |
|
| 995 |
st.divider()
|
| 996 |
|
|
|
|
| 1012 |
elif not uploaded_files:
|
| 1013 |
st.warning("β οΈ Upload files first.")
|
| 1014 |
else:
|
| 1015 |
+
file_hash = f"{compute_files_hash(uploaded_files)}-ocr-{use_ocr}-{ocr_page_limit}"
|
| 1016 |
force_reprocess = (file_hash != st.session_state.last_file_hash)
|
| 1017 |
|
| 1018 |
if not force_reprocess and st.session_state.vectors:
|
|
|
|
| 1023 |
t0 = time.time()
|
| 1024 |
|
| 1025 |
st.write("π₯ Loading files into memory...")
|
| 1026 |
+
raw_docs = load_documents(uploaded_files, use_ocr=use_ocr, ocr_page_limit=ocr_page_limit)
|
| 1027 |
|
| 1028 |
if not raw_docs:
|
| 1029 |
status.update(label="No content found!", state="error")
|
|
|
|
| 1075 |
|
| 1076 |
# BUG FIX: Use the saved text instead of raw_docs
|
| 1077 |
if not st.session_state.full_raw_text:
|
| 1078 |
+
temp_docs = load_documents(uploaded_files, use_ocr=use_ocr, ocr_page_limit=ocr_page_limit)
|
| 1079 |
st.session_state.full_raw_text = " ".join([d.page_content for d in temp_docs])
|
| 1080 |
|
| 1081 |
full_text = st.session_state.full_raw_text[:6000]
|
assets/.gitkeep
CHANGED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
|
|
|
|
|
|
assets/rag_architecture.svg
ADDED
|
|
eval_sample.csv
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
question,expected_answer,expected_source
|
| 2 |
+
What is the main purpose of this document?,Replace this with the expected answer from your uploaded document.,Document page or section
|
| 3 |
+
What are the most important risks mentioned?,Replace this with the expected risk statement.,Document page or section
|
| 4 |
+
Who are the key people or organizations mentioned?,Replace this with the expected names or organizations.,Document page or section
|
requirements.txt
CHANGED
|
@@ -6,8 +6,11 @@ langchain-community==0.2.16
|
|
| 6 |
langchain-groq==0.1.10
|
| 7 |
langchain-huggingface==0.0.3
|
| 8 |
langchain-text-splitters==0.2.4
|
| 9 |
-
faiss-cpu
|
| 10 |
-
|
| 11 |
pypdf==4.3.1
|
| 12 |
docx2txt==0.8
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
langchain-groq==0.1.10
|
| 7 |
langchain-huggingface==0.0.3
|
| 8 |
langchain-text-splitters==0.2.4
|
| 9 |
+
faiss-cpu
|
| 10 |
+
tiktoken==0.7.0
|
| 11 |
pypdf==4.3.1
|
| 12 |
docx2txt==0.8
|
| 13 |
+
sentence-transformers==3.1.1
|
| 14 |
+
pypdfium2==4.30.0
|
| 15 |
+
pytesseract==0.3.13
|
| 16 |
+
Pillow==10.4.0
|