feat: add Ollama runtime support and RAG implementation plan

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)
This commit is contained in:
Apple
2025-11-16 02:56:36 -08:00
parent d56ff3493d
commit 00f9102e50
6 changed files with 607 additions and 9 deletions

369
TODO-RAG.md Normal file
View File

@@ -0,0 +1,369 @@
# 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/)