Ollama Runtime: - Add ollama_client.py for Ollama API integration - Support for dots-ocr model via Ollama - Add OLLAMA_BASE_URL configuration - Update inference.py to support Ollama runtime (RUNTIME_TYPE=ollama) - Update endpoints to handle async Ollama calls - Alternative to local transformers model RAG Implementation Plan: - Create TODO-RAG.md with detailed Haystack integration plan - Document Store setup (pgvector) - Embedding model selection - Ingest pipeline (PARSER → RAG) - Query pipeline (RAG → LLM) - Integration with DAGI Router - Bot commands (/upload_doc, /ask_doc) - Testing strategy Now supports three runtime modes: 1. Local transformers (RUNTIME_TYPE=local) 2. Ollama (RUNTIME_TYPE=ollama) 3. Dummy (USE_DUMMY_PARSER=true)
370 lines
11 KiB
Markdown
370 lines
11 KiB
Markdown
# TODO — RAG Stack (Haystack + PARSER Agent)
|
||
|
||
Цей план описує, як побудувати RAG-шар навколо PARSER (dots.ocr) та DAGI Router.
|
||
|
||
**Статус:** 🟡 Планування
|
||
|
||
---
|
||
|
||
## 1. Document Store (pgvector або Qdrant)
|
||
|
||
### 1.1. Вибір бекенду
|
||
|
||
- [ ] Обрати бекенд:
|
||
- [ ] `Postgres + pgvector` (рекомендовано, якщо в нас уже є Postgres)
|
||
- [ ] або `Qdrant` (docker-сервіс)
|
||
|
||
**Рекомендація:** Використати `pgvector` (вже є в `city-db`)
|
||
|
||
### 1.2. Ініціалізація Haystack DocumentStore
|
||
|
||
Приклад для PostgreSQL + pgvector:
|
||
|
||
```python
|
||
# services/rag-service/app/document_store.py
|
||
|
||
from haystack.document_stores import PGVectorDocumentStore
|
||
|
||
def get_document_store() -> PGVectorDocumentStore:
|
||
return PGVectorDocumentStore(
|
||
connection_string="postgresql+psycopg2://postgres:postgres@city-db:5432/daarion_city",
|
||
embedding_dim=1024, # залежить від моделі ембеддингів
|
||
table_name="rag_documents",
|
||
search_strategy="approximate",
|
||
)
|
||
```
|
||
|
||
**Завдання:**
|
||
- [ ] Створити `services/rag-service/` структуру
|
||
- [ ] Додати `app/document_store.py` з ініціалізацією
|
||
- [ ] Налаштувати підключення до `city-db`
|
||
|
||
---
|
||
|
||
## 2. Embedding-модель
|
||
|
||
### 2.1. Обрати модель
|
||
|
||
- [ ] Вибрати embedding-модель:
|
||
- [ ] `BAAI/bge-m3` (multilingual, 1024 dim)
|
||
- [ ] `sentence-transformers/all-MiniLM-L12-v2` (легка, 384 dim)
|
||
- [ ] `intfloat/multilingual-e5-base` (українська підтримка, 768 dim)
|
||
|
||
**Рекомендація:** `BAAI/bge-m3` для кращої підтримки української
|
||
|
||
### 2.2. Обгортка під Haystack
|
||
|
||
```python
|
||
# services/rag-service/app/embedding.py
|
||
|
||
from haystack.components.embedders import SentenceTransformersTextEmbedder
|
||
|
||
def get_text_embedder():
|
||
return SentenceTransformersTextEmbedder(
|
||
model="BAAI/bge-m3",
|
||
device="cuda" # або "cpu"
|
||
)
|
||
```
|
||
|
||
**Завдання:**
|
||
- [ ] Створити `app/embedding.py`
|
||
- [ ] Додати конфігурацію моделі через env
|
||
- [ ] Тестувати на українському тексті
|
||
|
||
---
|
||
|
||
## 3. Ingest-пайплайн: PARSER → RAG
|
||
|
||
### 3.1. Функція ingest_document
|
||
|
||
- [ ] Створити `services/rag-service/app/ingest_pipeline.py`:
|
||
|
||
```python
|
||
# services/rag-service/app/ingest_pipeline.py
|
||
|
||
from haystack import Pipeline
|
||
from haystack.components.preprocessors import DocumentSplitter
|
||
from haystack.components.writers import DocumentWriter
|
||
from haystack.schema import Document
|
||
|
||
from .document_store import get_document_store
|
||
from .embedding import get_text_embedder
|
||
|
||
# 1) splitter — якщо треба додатково різати текст
|
||
splitter = DocumentSplitter(
|
||
split_by="sentence",
|
||
split_length=8,
|
||
split_overlap=1
|
||
)
|
||
|
||
embedder = get_text_embedder()
|
||
doc_store = get_document_store()
|
||
writer = DocumentWriter(document_store=doc_store)
|
||
|
||
ingest_pipeline = Pipeline()
|
||
ingest_pipeline.add_component("splitter", splitter)
|
||
ingest_pipeline.add_component("embedder", embedder)
|
||
ingest_pipeline.add_component("writer", writer)
|
||
|
||
ingest_pipeline.connect("splitter.documents", "embedder.documents")
|
||
ingest_pipeline.connect("embedder.documents", "writer.documents")
|
||
|
||
|
||
def ingest_parsed_document(dao_id: str, doc_id: str, parsed_json: dict):
|
||
"""
|
||
parsed_json — результат PARSER (mode=raw_json або qa_pairs/chunks).
|
||
Тут треба перетворити його у список haystack.Document.
|
||
"""
|
||
blocks = parsed_json.get("blocks", [])
|
||
docs = []
|
||
|
||
for b in blocks:
|
||
text = b.get("text") or ""
|
||
if not text.strip():
|
||
continue
|
||
|
||
meta = {
|
||
"dao_id": dao_id,
|
||
"doc_id": doc_id,
|
||
"page": b.get("page"),
|
||
"section_type": b.get("type"),
|
||
}
|
||
|
||
docs.append(Document(content=text, meta=meta))
|
||
|
||
if not docs:
|
||
return
|
||
|
||
ingest_pipeline.run(
|
||
{
|
||
"splitter": {"documents": docs}
|
||
}
|
||
)
|
||
```
|
||
|
||
**Завдання:**
|
||
- [ ] Створити ingest pipeline
|
||
- [ ] Додати конвертацію ParsedDocument → Haystack Documents
|
||
- [ ] Додати обробку chunks mode (якщо PARSER повертає готові chunks)
|
||
|
||
### 3.2. Інтеграція з PARSER Service
|
||
|
||
- [ ] Додати виклик `parser-service` у DevTools / CrewAI workflow:
|
||
- [ ] Завантажити файл
|
||
- [ ] Викликати `/ocr/parse?output_mode=raw_json` або `/ocr/parse_chunks`
|
||
- [ ] Передати `parsed_json` у `ingest_parsed_document`
|
||
|
||
**Завдання:**
|
||
- [ ] Створити `services/rag-service/app/parser_client.py` для виклику parser-service
|
||
- [ ] Додати endpoint `/rag/ingest` для завантаження документів
|
||
- [ ] Інтегрувати з Gateway для команди `/upload_doc`
|
||
|
||
---
|
||
|
||
## 4. Query-пайплайн: питання → RAG → LLM
|
||
|
||
### 4.1. Retriever + Generator
|
||
|
||
```python
|
||
# services/rag-service/app/query_pipeline.py
|
||
|
||
from haystack import Pipeline
|
||
from haystack.components.retrievers import DocumentRetriever
|
||
from haystack.components.generators import OpenAIGenerator # або свій LLM через DAGI Router
|
||
from .document_store import get_document_store
|
||
from .embedding import get_text_embedder
|
||
|
||
doc_store = get_document_store()
|
||
embedder = get_text_embedder()
|
||
|
||
retriever = DocumentRetriever(document_store=doc_store)
|
||
# У проді замінити на кастомний generator, що ходить у DAGI Router
|
||
generator = OpenAIGenerator(
|
||
api_key="DUMMY",
|
||
model="gpt-4o-mini"
|
||
)
|
||
|
||
query_pipeline = Pipeline()
|
||
query_pipeline.add_component("embedder", embedder)
|
||
query_pipeline.add_component("retriever", retriever)
|
||
query_pipeline.add_component("generator", generator)
|
||
|
||
query_pipeline.connect("embedder.documents", "retriever.documents")
|
||
query_pipeline.connect("retriever.documents", "generator.documents")
|
||
|
||
|
||
def answer_query(dao_id: str, question: str):
|
||
filters = {"dao_id": [dao_id]}
|
||
|
||
result = query_pipeline.run(
|
||
{
|
||
"embedder": {"texts": [question]},
|
||
"retriever": {"filters": filters},
|
||
"generator": {"prompt": question},
|
||
}
|
||
)
|
||
|
||
answer = result["generator"]["replies"][0]
|
||
documents = result["retriever"]["documents"]
|
||
return answer, documents
|
||
```
|
||
|
||
**У реальному стеку:**
|
||
- Генератором буде не OpenAI, а DAGI Router (через окремий компонент / кастомний генератор)
|
||
- Фільтри по `dao_id`, `roles`, `visibility` будуть інтегровані з RBAC
|
||
|
||
**Завдання:**
|
||
- [ ] Створити query pipeline
|
||
- [ ] Додати кастомний generator для DAGI Router
|
||
- [ ] Додати RBAC фільтри
|
||
- [ ] Створити endpoint `/rag/query`
|
||
|
||
---
|
||
|
||
## 5. Інтеграція з DAGI Router
|
||
|
||
### 5.1. Режим `mode=rag_query`
|
||
|
||
- [ ] Додати у `router-config.yml` rule:
|
||
|
||
```yaml
|
||
routing:
|
||
- id: rag_query
|
||
when:
|
||
mode: rag_query
|
||
use_provider: llm_local_qwen3_8b # або окремий RAG-provider
|
||
```
|
||
|
||
- [ ] Додати handler у `RouterApp`, який:
|
||
- До виклику LLM запускає `answer_query(dao_id, question)`
|
||
- В prompt LLM додає витягнуті документи як контекст
|
||
|
||
**Завдання:**
|
||
- [ ] Оновити `router-config.yml`
|
||
- [ ] Додати RAG provider в Router
|
||
- [ ] Створити handler для `mode=rag_query`
|
||
|
||
---
|
||
|
||
## 6. RAG Service (FastAPI)
|
||
|
||
### 6.1. Структура сервісу
|
||
|
||
- [ ] Створити `services/rag-service/`:
|
||
- [ ] `app/main.py` - FastAPI додаток
|
||
- [ ] `app/api/endpoints.py` - ендпоінти:
|
||
- [ ] `POST /rag/ingest` - інжест документу
|
||
- [ ] `POST /rag/query` - запит до RAG
|
||
- [ ] `GET /rag/health` - health check
|
||
- [ ] `app/schemas.py` - Pydantic моделі
|
||
- [ ] `requirements.txt` - залежності (haystack, pgvector, etc.)
|
||
- [ ] `Dockerfile`
|
||
|
||
### 6.2. Ендпоінти
|
||
|
||
```python
|
||
# services/rag-service/app/api/endpoints.py
|
||
|
||
@router.post("/rag/ingest")
|
||
async def ingest_document_endpoint(
|
||
doc_id: str,
|
||
dao_id: str,
|
||
parsed_doc: ParsedDocument # або doc_url для завантаження
|
||
):
|
||
"""Ingest parsed document into RAG"""
|
||
# Викликати ingest_parsed_document()
|
||
pass
|
||
|
||
@router.post("/rag/query")
|
||
async def query_endpoint(
|
||
dao_id: str,
|
||
question: str,
|
||
user_id: str
|
||
):
|
||
"""Query RAG and return answer with citations"""
|
||
# Викликати answer_query()
|
||
# Повернути відповідь + цитати
|
||
pass
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Інтеграція з DAARWIZZBot / microDAO
|
||
|
||
### 7.1. Команди для бота
|
||
|
||
- [ ] Додати команди в `gateway-bot/http_api.py`:
|
||
- [ ] `/upload_doc` → інжест документу в RAG через PARSER
|
||
- [ ] Підтримка завантаження файлів через Telegram
|
||
- [ ] Виклик `parser-service` → `rag-service`
|
||
- [ ] `/ask_doc` → питання до бази документів DAO
|
||
- [ ] Виклик `rag-service` → DAGI Router
|
||
- [ ] Відправка відповіді з цитатами
|
||
|
||
### 7.2. RBAC
|
||
|
||
- [ ] Хто може інжестити документи (`role: admin`, `role: researcher`)
|
||
- [ ] Хто може ставити питання до приватних документів
|
||
- [ ] Перевірка прав в `microdao/rbac.py`
|
||
|
||
---
|
||
|
||
## 8. Тести
|
||
|
||
- [ ] Інжест одного PDF (наприклад, "Токеноміка MicroDAO") через PARSER → ingest
|
||
- [ ] Питання:
|
||
> "Поясни, як працює стейкінг у цьому microDAO."
|
||
- [ ] Перевірити, що Haystack знаходить потрібні фрагменти і LLM будує відповідь з цитатами
|
||
|
||
**Завдання:**
|
||
- [ ] Створити тестові фікстури (PDF документи)
|
||
- [ ] E2E тести для ingest → query
|
||
- [ ] Тести на RBAC фільтри
|
||
|
||
---
|
||
|
||
## Порядок виконання (рекомендований)
|
||
|
||
### Фаза 1: Document Store + Embeddings (1-2 дні)
|
||
1. Налаштувати pgvector в city-db
|
||
2. Створити Haystack DocumentStore
|
||
3. Вибрати та налаштувати embedding-модель
|
||
|
||
### Фаза 2: Ingest Pipeline (2-3 дні)
|
||
1. Створити ingest pipeline
|
||
2. Інтегрувати з PARSER Service
|
||
3. Створити RAG Service з endpoint `/rag/ingest`
|
||
|
||
### Фаза 3: Query Pipeline (2-3 дні)
|
||
1. Створити query pipeline
|
||
2. Інтегрувати з DAGI Router
|
||
3. Додати RBAC фільтри
|
||
|
||
### Фаза 4: Інтеграція з ботом (1-2 дні)
|
||
1. Додати команди `/upload_doc`, `/ask_doc`
|
||
2. Тестування E2E
|
||
|
||
**Загальний час:** ~6-10 днів
|
||
|
||
---
|
||
|
||
## Залежності
|
||
|
||
### Python пакети
|
||
- `haystack-ai>=2.0.0`
|
||
- `sentence-transformers>=2.2.0`
|
||
- `pgvector>=0.2.0`
|
||
- `psycopg2-binary>=2.9.0`
|
||
|
||
### Системні залежності
|
||
- PostgreSQL з pgvector (вже є в city-db)
|
||
|
||
---
|
||
|
||
## Посилання
|
||
|
||
- [PARSER Agent Documentation](./docs/agents/parser.md)
|
||
- [TODO: PARSER Implementation](./TODO-PARSER-RAG.md)
|
||
- [Haystack Documentation](https://docs.haystack.deepset.ai/)
|
||
|