Files
microdao-daarion/TODO-RAG.md
Apple 00f9102e50 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)
2025-11-16 02:56:36 -08:00

11 KiB
Raw Blame History

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:

# 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

# 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:
# 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

# 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:
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. Ендпоінти

# 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-servicerag-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)

Посилання