Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,58 @@ name: CI

on:
push:
branches: [main]
branches: [main, production-revamp]
pull_request:
branches: [main]

jobs:
# ── Backend: lint → test → build image ─────────────────────
backend:
name: Build backend image
name: Backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build

- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install deps
run: |
pip install -r requirements.txt
pip install ruff pytest httpx pytest-asyncio

- name: Lint
run: ruff check services/ documind_main.py

- name: Test
run: pytest tests/ -v

- name: Build image
run: docker build -f Dockerfile.backend -t documind-backend .

# ── Frontend: lint → test → build image ────────────────────
frontend:
name: Build frontend image
name: Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build

- uses: actions/setup-node@v4
with:
node-version: "18"
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install deps
run: cd frontend && npm ci --legacy-peer-deps

- name: Lint
run: cd frontend && npx eslint src/ --max-warnings=0

- name: Test
run: cd frontend && CI=false npm test -- --watchAll=false --ci

- name: Build image
run: docker build -f Dockerfile.frontend -t documind-frontend .
16 changes: 10 additions & 6 deletions Dockerfile.backend
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
FROM python:3.11-slim
# ── Stage 1: install deps ────────────────────────────────────
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ── Stage 2: runtime ─────────────────────────────────────────
FROM python:3.11-slim AS runtime
WORKDIR /app

# Install deps before copying app code for better layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --from=builder /install /usr/local

# Non-root user
RUN useradd --create-home appuser
USER appuser

COPY --chown=appuser:appuser . .
COPY --chown=appuser:appuser services/ ./services/
COPY --chown=appuser:appuser documind_main.py .

EXPOSE 8000

Expand Down
44 changes: 33 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@ Query → embed question → vector search → rerank → LLM → answer with ci

## Running locally

**With Docker (recommended):**

```bash
docker-compose up
```

Frontend at `http://localhost:3000`, API at `http://localhost:8000`.

**Without Docker:**

```bash
pip install -r requirements.txt
python minimal_main.py
python documind_main.py
```

App starts at `http://localhost:8000`. API docs at `/docs`.
Expand Down Expand Up @@ -74,22 +84,34 @@ BM25_WEIGHT = 0.3

```
DocuMindHTech-main/
├── minimal_main.py # Entry point (Render-ready, no heavy deps)
├── documind_main.py # Full version with React frontend support
├── requirements.txt
├── render.yaml
├── frontend/
│ └── index.html
├── documind_main.py # Entry point — serves API + React SPA
├── requirements.txt # Runtime deps
├── requirements-dev.txt # Dev/test deps (pytest, ruff, httpx)
├── Dockerfile.backend # Multi-stage Python image
├── Dockerfile.frontend # Multi-stage nginx image
├── docker-compose.yml # Run backend + frontend together
├── render.yaml # Render deployment config
├── tests/
│ └── test_main.py # pytest suite for all API endpoints
├── frontend/ # React CRA application
│ ├── src/
│ │ ├── App.js
│ │ └── components/
│ │ ├── LandingView.jsx
│ │ ├── UploadView.jsx
│ │ ├── QueryView.jsx
│ │ ├── UploadView.test.jsx
│ │ └── QueryView.test.jsx
│ └── package.json
└── services/
├── routes.py
├── api_service.py # Orchestrates the pipeline
├── routes.py # Full RAG API router (uses api_service)
├── api_service.py # Pipeline orchestrator
├── chunking_service.py
├── reranker_service.py
├── citation_service.py
├── cloud_vector_service.py
├── embedding_service.py
├── chat_service.py
├── text_extract.py
├── text_extract.py # Multi-format extraction with OCR
└── image_analyze.py
```

Expand Down
11 changes: 10 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ services:
DB_PATH: "/app/data/documind.db"
volumes:
- db_data:/app/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s

frontend:
build:
Expand All @@ -23,7 +30,9 @@ services:
ports:
- "3000:80"
depends_on:
- backend
backend:
condition: service_healthy
restart: unless-stopped

volumes:
db_data:
130 changes: 31 additions & 99 deletions documind_main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""
Production entry point. Serves the React frontend if a build exists, otherwise falls back to frontend/index.html.
Production entry point. Serves the React frontend if a build exists.
API and health routes are always registered before the SPA catch-all
so they are never shadowed by the wildcard path handler.
"""

import os
import uuid
import uvicorn
from typing import Optional
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse
from dotenv import load_dotenv

load_dotenv()
Expand All @@ -21,35 +21,18 @@ def create_app():
description="RAG pipeline: upload documents, ask questions, get cited answers.",
version="2.0.0",
docs_url="/docs",
redoc_url="/redoc"
redoc_url="/redoc",
)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

frontend_build_path = "frontend/build"
if os.path.exists(frontend_build_path):
print("Mounting React frontend from build directory")
app.mount("/static", StaticFiles(directory=f"{frontend_build_path}/static"), name="static")

@app.get("/{full_path:path}")
async def serve_react_app(full_path: str):
if full_path.startswith(("api/", "docs", "redoc", "health")):
raise HTTPException(status_code=404, detail="Not found")
if os.path.exists(f"{frontend_build_path}/index.html"):
return FileResponse(f"{frontend_build_path}/index.html")
raise HTTPException(status_code=404, detail="Frontend not found")
else:
print("React build not found, falling back to frontend/index.html")

@app.get("/")
async def root():
return FileResponse("frontend/index.html")
# ── System / health routes ────────────────────────────────
# Registered BEFORE the SPA catch-all so they are never shadowed.

@app.get("/health")
async def health_check():
Expand All @@ -64,78 +47,31 @@ async def favicon():
from fastapi.responses import Response
return Response(content="", media_type="image/x-icon")

@app.get("/api")
async def api_info():
return {
"status": "running",
"endpoints": {
"health": "/health",
"upload": "/api/upload",
"query": "/api/query",
"docs": "/docs",
}
}
# ── API routes ────────────────────────────────────────────

@app.get("/api/health")
async def api_health():
return {"status": "healthy"}
from services.routes import router as api_router
app.include_router(api_router)

@app.get("/api/test")
async def test_endpoint():
return {
"status": "ok",
"mode": "minimal",
"note": "Set API keys and configure a vector database for full functionality.",
}
# ── Frontend static files / SPA catch-all (registered LAST) ──

frontend_build_path = "frontend/build"
if os.path.exists(frontend_build_path):
app.mount(
"/static",
StaticFiles(directory=f"{frontend_build_path}/static"),
name="static",
)

@app.get("/api/debug")
async def debug_endpoint():
try:
return {
"frontend_directory_exists": os.path.exists("frontend"),
"index_html_exists": os.path.exists("frontend/index.html"),
"build_directory_exists": os.path.exists("frontend/build"),
"current_directory": os.getcwd(),
"directory_contents": os.listdir("."),
}
except Exception as e:
return {"error": str(e)}

@app.post("/api/upload")
async def upload_endpoint(
file: Optional[UploadFile] = File(None),
text: Optional[str] = Form(None)
):
if not file and not text:
raise HTTPException(status_code=400, detail="Either file or text must be provided")
document_id = str(uuid.uuid4())
return JSONResponse({
"success": True,
"document_id": document_id,
"status": "minimal_mode",
"note": "Full functionality requires API keys and a configured vector database.",
})

@app.post("/api/query")
async def query_endpoint(request: Request):
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")

if not body or "question" not in body or "document_id" not in body:
raise HTTPException(status_code=400, detail="question and document_id are required")

question = body.get("question", "").strip()
if not question:
raise HTTPException(status_code=400, detail="question cannot be empty")

document_id = body.get("document_id", "")
return JSONResponse({
"success": True,
"answer": f"Demo response to: '{question}' (document: {document_id}). Full mode requires API keys and a vector database.",
"status": "minimal_mode",
})
@app.get("/{full_path:path}")
async def serve_react_app(full_path: str):
index = f"{frontend_build_path}/index.html"
if os.path.exists(index):
return FileResponse(index)
raise HTTPException(status_code=404, detail="Frontend not found")
else:
@app.get("/")
async def root():
return FileResponse("frontend/index.html")

return app

Expand All @@ -153,8 +89,4 @@ async def query_endpoint(request: Request):
print(f"UI: http://{host}:{port}/")
print(f"Health: http://{host}:{port}/health")

try:
uvicorn.run("documind_main:app", host=host, port=port, reload=reload, log_level="info")
except Exception as e:
print(f"Failed to start: {e}")
exit(1)
uvicorn.run("documind_main:app", host=host, port=port, reload=reload, log_level="info")
7 changes: 7 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');

:root {
--bg: #080808;
--glow-color: rgba(100, 140, 100, 0.12);
Expand All @@ -11,6 +13,11 @@
--text-muted: rgba(255, 255, 255, 0.25);
--radius-card: 16px;
--radius-pill: 999px;

/* Amber accent */
--accent: #E8933A;
--accent-dim: rgba(232, 147, 58, 0.10);
--accent-border: rgba(232, 147, 58, 0.38);
}

html, body, #root {
Expand Down
Loading
Loading