Skip to main content
Glama
fayna-digital

fayna-rag-mcp

Official
README.md
# fayna-rag-mcp — lokalna baza wiedzy z RAG i MCP

![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
![License](https://img.shields.io/badge/License-MIT-green.svg)
![Status](https://img.shields.io/badge/status-demo%20%2F%20portfolio-orange)

**Opracowane przez [Fayna Digital](https://www.fayna.agency)**
**Autor: Volodymyr Shevchenko**

---

**Problem:** zespół gromadzi dokumentację (polityki, podręczniki, notatki) w
plikach i szybko traci do niej dostęp w języku naturalnym — wyszukiwanie po
nazwie pliku czy Ctrl+F nie skaluje się, a wysyłanie wewnętrznych dokumentów do
chmurowego serwisu LLM nie zawsze jest akceptowalne ze względu na prywatność.

**Rozwiązanie:** lokalny potok RAG (FAISS + wielojęzyczne embeddingi) na bazie
lokalnego LLM (Ollama) — a ten sam wyszukiwacz/Q&A wystawiony jako **serwer MCP**,
aby mógł z niego korzystać dowolny klient MCP (Claude Desktop/Code itp.) lub
zewnętrzny zautomatyzowany workflow przez proste trasy REST. **Żadne dane nie
opuszczają maszyny, na której to działa.**

**Rezultat:** pięć narzędzi MCP i cztery trasy REST do wyszukiwania
semantycznego, RAG Q&A ze źródłami, czytania dokumentów i offline-katalogowania —
gotowe do podłączenia do Claude czy n8n w kilka minut, bez zależności chmurowych.

## Możliwości

| Narzędzie (MCP) | Przeznaczenie |
|---|---|
| `read_document(file_path)` | odczyt pełnego pliku z bazy (z guardem na path-traversal w obrębie `DOCUMENTS_DIR`) |
| `list_documents()` | lista wszystkich dokumentów (`.txt`, `.md`, `.pdf`, `.docx`) |
| `search_documents(query)` | wyszukiwanie semantyczne FAISS → top-relewantne chunki |
| `ask_knowledge_base(question)` | odpowiedź RAG: FAISS-retrieve + Ollama LLM, ze źródłami |
| `show_catalog()` | read-only katalog tagów dokumentów (temat/typ/język/audytorium) |

| Trasa REST | Body | Co robi |
|---|---|---|
| `POST /search` | `{"query": …}` | FAISS-retrieve → `{results:[{source,text}]}` |
| `POST /ask` | `{"question": …}` | odpowiedź RAG → `{answer, sources}` |
| `POST /find` | `{"query": …}` | dokładne wyszukiwanie podciągu po `.md`/`.txt` w korpusie |
| `POST /hybrid` | `{"query": …}` | hybryda: transliteracja cyrylica↔łacinka + token-match, dobór semantyką |

## Stack

Python 3.10+ · FAISS (`faiss-cpu`) · `sentence-transformers` · `tiktoken` ·
Ollama · FastMCP · Tesseract/poppler/Whisper do multi-formatowego ingestu ·
Docker.

## Potok RAG

```
docs/ → load (.txt/.md/.pdf/.docx) → chunk (tiktoken) → embed (mpnet) → FAISS → retrieve → Ollama → answer + sources
```

- **Chunking — po tokenach `tiktoken` (`cl100k_base`), nie po znakach.** Domyślnie
  `CHUNK_SIZE=700` tokenów, `CHUNK_OVERLAP=100` tokenów.
- **Embeddingi:** `paraphrase-multilingual-mpnet-base-v2` — model wielojęzyczny
  (UA/PL/EN/RU i inne), aby wyszukiwanie działało niezależnie od języka zapytania.
- **LLM:** dowolny model Ollama, domyślnie `qwen2.5:7b`.
- **Indeks:** FAISS `IndexFlatIP` (bliskość kosinusowa), `TOP_K=5`.

## Szybki start

```bash
pip install -r src/requirements.txt

# Przykład: demo-korpus na kilka dokumentów (sample-docs/)
export DOCUMENTS_DIR=./sample-docs
python -m src.main build-index      # → src/index/index.faiss + chunks.pkl

# Interaktywne Q&A (CLI)
python -m src.main

# Serwer MCP (transport z env MCP_TRANSPORT: stdio|http)
python -m src.mcp.server
```

Testy:

```bash
pip install -r tests/requirements-dev.txt
pytest -q
```

## Docker

```bash
docker compose up -d
```

Domyślne wartości w `docker-compose.yml`/`Dockerfile` są zaprojektowane pod Ollama
działające na hoście (przez `host.docker.internal`); dopasuj `OLLAMA_URL` do
własnej sieci (adres bridge na Linuxie, osobny kontener Ollama itp.).

## Konfiguracja (`src/config.py`, wszystko przez env)

| Env | Domyślnie | Opis |
|---|---|---|
| `DOCUMENTS_DIR` | `./docs` | korzeń bazy wiedzy |
| `EMBEDDING_MODEL` | `paraphrase-multilingual-mpnet-base-v2` | model embeddingów |
| `OLLAMA_MODEL` | `qwen2.5:7b` | LLM do odpowiedzi RAG |
| `OLLAMA_URL` | `http://localhost:11434/api/generate` | endpoint Ollama |
| `CHUNK_SIZE` / `CHUNK_OVERLAP` | `700` / `100` | tokeny (`tiktoken`), nie znaki |
| `TOP_K` | `5` | ile chunków zwraca retrieve |
| `MCP_TRANSPORT` | `stdio` | `stdio` (dla lokalnego klienta MCP) lub `http` → FastMCP streamable-http |
| `MCP_HOST` / `MCP_PORT` | `0.0.0.0` / `8765` | adres przy `MCP_TRANSPORT=http` |

Podłączenie do dowolnego klienta MCP — przez jego konfigurację serwerów MCP,
komendą `python -m src.mcp.server` (stdio) lub URL kontenera (http).

## Struktura

```
src/
├── config.py         # wszystkie env-zmienne + domyślne
├── main.py           # CLI: build-index | interaktywne Q&A
├── assistant.py       # CompanyKBAssistant (LLM decyduje czy wołać MCP-toolki)
├── catalog.py         # offline-klasyfikacja dokumentów przez Ollama → JSON+HTML
├── ingest.py           # multi-formatowy ingest: OCR skanów, vision-opis diagramów, Whisper-transkrypcja
├── rag/
│   ├── ingest.py      # load_document (.txt/.md/.pdf/.docx)
│   ├── chunk.py        # chunk_text (tiktoken cl100k_base, overlap)
│   ├── embed.py         # embed_chunks (sentence-transformers)
│   ├── build_index.py   # build_index → FAISS + pickle
│   └── query.py          # retrieve / build_prompt / ask
└── mcp/
    ├── server.py     # FastMCP: 5 MCP-toolków + 4 trasy REST
    └── client.py      # MCPClient (JSON-RPC przez subprocess)
```

Multi-formatowy ingest (OCR skanów przez Tesseract, opis rysunków/schematów przez
model vision, transkrypcja audio/wideo przez Whisper) — osobna, cięższa pod
względem zależności ścieżka, niepotrzebna dla podstawowego korpusu tekstowego powyżej.

## Licencja

MIT — patrz [LICENSE](LICENSE). © Fayna Digital.