OCR/VLM RAG API: aplikacja typu REST API w FastAPI, która przyjmuje obrazy dokumentów (faktury, paragony, formularze), odczytuje ich zawartość za pomocą modelu GLM (vision-language) przez OpenRouter, a następnie pozwala wyszukiwać informacje i zadawać pytania z wykorzystaniem podejścia RAG.
Projekt składa się z czterech serwisów:
| Serwis | Rola |
|---|---|
| api | FastAPI — obsługuje endpointy REST, upload obrazów, indeksowanie w ChromaDB, wyszukiwanie i odpowiedzi RAG |
| worker | Celery — przetwarza dokumenty w tle (wywołuje GLM przez OpenRouter, zapisuje wyniki do SQLite) |
| redis | Broker wiadomości Celery + backend wyników |
| chroma | ChromaDB w trybie server — baza wektorowa do indeksu semantycznego |
obraz dokumentu → POST /documents/upload → Celery task → GLM (OpenRouter) → tekst + JSON
→ POST /documents/{id}/index → embedding (OpenAI) → ChromaDB
→ POST /rag/search lub /rag/answer → wyszukiwanie semantyczne → odpowiedź RAG (GPT-4.1)
document_rag/
├── api/ # Serwis FastAPI
│ ├── main.py # Punkt wejścia aplikacji
│ ├── config.py # Konfiguracja (zmienne środowiskowe)
│ ├── database.py # SQLAlchemy engine + session
│ ├── models.py # Model ORM (Document)
│ ├── schemas.py # Schematy Pydantic (request/response)
│ ├── celery_app.py # Klient Celery (send_task)
│ ├── rag.py # Logika RAG (ChromaDB + LangChain)
│ ├── endpoints/ # Endpointy API
│ │ ├── health.py # GET /health
│ │ ├── documents.py # POST /upload, GET /{id}, POST /{id}/index
│ │ └── rag.py # POST /search, POST /answer
│ ├── Dockerfile
│ └── requirements.txt
├── worker/ # Serwis Celery worker
│ ├── tasks.py # Task: process_document
│ ├── vlm.py # Integracja z OpenRouter GLM
│ ├── celery_app.py # Instancja workera Celery
│ ├── config.py # Konfiguracja workera
│ ├── _models.py # Model SQLAlchemy (mirror api/models.py)
│ ├── database.py # Połączenie z SQLite
│ ├── Dockerfile
│ └── requirements.txt
├── k8s/ # Manifesty Kubernetes
├── docker-compose.yml
├── .dockerignore
├── .env.example
├── download_invoices.py # Skrypt pobierania danych z HuggingFace
└── prd.md # Specyfikacja projektu
| Metoda | Ścieżka | Opis | Status HTTP |
|---|---|---|---|
GET |
/health |
Sprawdzenie stanu aplikacji | 200 |
POST |
/documents/upload |
Upload obrazu dokumentu | 202 Accepted |
GET |
/documents/{document_id} |
Status przetwarzania dokumentu | 200 / 404 |
POST |
/documents/{document_id}/index |
Dodanie dokumentu do indeksu RAG | 200 / 404 / 409 |
POST |
/rag/search |
Wyszukiwanie semantyczne w dokumentach | 200 |
POST |
/rag/answer |
Pytanie do dokumentów z odpowiedzią RAG | 200 |
- Docker + Docker Compose
- Klucz API OpenAI (embeddingi + GPT-4o-mini)
- Klucz API OpenRouter (model GLM do ekstrakcji dokumentów)
Projekt korzysta z datasetu katanaml-org/invoices-donut-data-v1.
pip install datasets Pillow
python download_invoices.pyObrazy zostaną zapisane w katalogu data/.
Skopiuj plik .env.example do .env i uzupełnij klucze API:
cp .env.example .env
# Edytuj .env — wpisz OPENAI_API_KEY i OPENROUTER_API_KEYdocker compose up --buildAplikacja będzie dostępna pod adresem: http://localhost:8000
Dokumentacja Swagger UI: http://localhost:8000/docs
# Upload dokumentu
curl -X POST http://localhost:8000/documents/upload \
-F "file=@data/0000.png"
# Sprawdzenie statusu
curl http://localhost:8000/documents/{document_id}
# Indeksowanie dokumentu (po zakończeniu przetwarzania)
curl -X POST http://localhost:8000/documents/{document_id}/index
# Wyszukiwanie
curl -X POST http://localhost:8000/rag/search \
-H "Content-Type: application/json" \
-d '{"query": "Jaka jest kwota brutto?", "top_k": 5}'
# Pytanie z odpowiedzią RAG
curl -X POST http://localhost:8000/rag/answer \
-H "Content-Type: application/json" \
-d '{"question": "Kto jest sprzedawcą na fakturze?"}'docker compose downAby usunąć również wolumeny (dane):
docker compose down -vminikube start
eval $(minikube docker-env)docker build -t document-rag-api:latest -f api/Dockerfile .
docker build -t document-rag-worker:latest -f worker/Dockerfile .Edytuj k8s/secret.yaml — wpisz swoje klucze API, a następnie:
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/minikube service api -n document-ragkubectl port-forward -n document-rag svc/api 8000:8000
# Upload dokumentu
curl -X POST http://localhost:8000/documents/upload \
-F "file=@data/0000.png"
# Sprawdzenie statusu
curl http://localhost:8000/documents/{document_id}
# Indeksowanie dokumentu (po zakończeniu przetwarzania)
curl -X POST http://localhost:8000/documents/{document_id}/index
# Wyszukiwanie
curl -X POST http://localhost:8000/rag/search \
-H "Content-Type: application/json" \
-d '{"query": "Jaka jest kwota brutto?", "top_k": 5}'
# Pytanie z odpowiedzią RAG
curl -X POST http://localhost:8000/rag/answer \
-H "Content-Type: application/json" \
-d '{"question": "Kto jest sprzedawcą na fakturze?"}'Dockerfile to plik tekstowy zawierający instrukcje do automatycznego budowania obrazu Docker. Każda instrukcja (FROM, COPY, RUN, CMD) definiuje jeden krok budowy. Docker czyta Dockerfile od góry do dołu i wykonuje instrukcje sekwencyjnie, tworząc obraz warstwa po warstwie.
Plik .dockerignore działa analogicznie do .gitignore — określa, które pliki i katalogi mają być wykluczone z kontekstu budowania (build context) wysyłanego do demona Docker. Dzięki temu:
- Obraz nie zawiera niepotrzebnych plików (np.
.git,.venv,data/) - Budowanie jest szybsze (mniejszy kontekst)
- Obraz jest mniejszy i bezpieczniejszy (np. nie zawiera
.envz kluczami API)
Docker context (kontekst budowania) to zestaw plików i katalogów, które są wysyłane do demona Docker podczas budowania obrazu. Domyślnie jest to katalog, w którym znajduje się Dockerfile (lub katalog podany jako argument docker build). Wszystkie instrukcje COPY i ADD odwołują się do plików w ramach tego kontekstu.
W naszym projekcie kontekstem budowania jest katalog główny (document_rag/), a Dockerfile wskazujemy flagą -f:
docker build -t document-rag-api:latest -f api/Dockerfile .Każda instrukcja w Dockerfile tworzy nową warstwę (layer) obrazu. Warstwy są cachowane — jeśli instrukcja i jej dane wejściowe się nie zmieniły, Docker używa wersji z cache zamiast wykonywać ją ponownie.
Warstwy są addytywne: każda kolejna dodaje zmiany (pliki, pakiety) na wierzch poprzedniej. Finalny obraz to złożenie wszystkich warstw.
-
Wykorzystuj cache warstw — instrukcje, które zmieniają się rzadko (np.
COPY requirements.txt+RUN pip install), umieszczaj wcześniej. Instrukcje zmieniające się często (np.COPY . .z kodem źródłowym) — na końcu. -
Używaj
.dockerignore— zmniejsz kontekst budowania, wykluczając niepotrzebne pliki. -
Używaj
--no-cache-dirw pip — zapobiega zapisywaniu cache pip wewnątrz obrazu. -
Wybieraj lekkie obrazy bazowe — np.
python:3.11-slimzamiastpython:3.11. -
Łącz instrukcje RUN — mniejsza liczba warstw, np.
RUN apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/*.
Docker buduje obraz warstwa po warstwie. Jeśli jedna warstwa się zmieni, wszystkie kolejne muszą być przebudowane (cache jest unieważniany od tego miejsca w dół).
Dlatego w naszych Dockerfile:
# 1. Kopiujemy TYLKO requirements.txt
COPY api/requirements.txt .
# 2. Instalujemy zależności (ta warstwa jest cachowana,
# dopóki requirements.txt się nie zmieni)
RUN pip install --no-cache-dir -r requirements.txt
# 3. Dopiero teraz kopiujemy kod źródłowy
COPY api/ ./api/Gdybyśmy skopiowali cały kod przed instalacją zależności (COPY . . → RUN pip install), każda zmiana w kodzie wymuszałaby ponowną instalację wszystkich pakietów. Dzięki rozdzieleniu tych kroków, zmiana kodu nie unieważnia cache warstwy z zależnościami.