- Vision Encoder Service (OpenCLIP ViT-L/14, GPU-accelerated)
- FastAPI app with text/image embedding endpoints (768-dim)
- Docker support with NVIDIA GPU runtime
- Port 8001, health checks, model info API
- Qdrant Vector Database integration
- Port 6333/6334 (HTTP/gRPC)
- Image embeddings storage (768-dim, Cosine distance)
- Auto collection creation
- Vision RAG implementation
- VisionEncoderClient (Python client for API)
- Image Search module (text-to-image, image-to-image)
- Vision RAG routing in DAGI Router (mode: image_search)
- VisionEncoderProvider integration
- Documentation (5000+ lines)
- SYSTEM-INVENTORY.md - Complete system inventory
- VISION-ENCODER-STATUS.md - Service status
- VISION-RAG-IMPLEMENTATION.md - Implementation details
- vision_encoder_deployment_task.md - Deployment checklist
- services/vision-encoder/README.md - Deployment guide
- Updated WARP.md, INFRASTRUCTURE.md, Jupyter Notebook
- Testing
- test-vision-encoder.sh - Smoke tests (6 tests)
- Unit tests for client, image search, routing
- Services: 17 total (added Vision Encoder + Qdrant)
- AI Models: 3 (qwen3:8b, OpenCLIP ViT-L/14, BAAI/bge-m3)
- GPU Services: 2 (Vision Encoder, Ollama)
- VRAM Usage: ~10 GB (concurrent)
Status: Production Ready ✅
12 KiB
Task: RAG ingestion — Wave 1 (Chat messages, Docs, Files)
Goal
Підключити першу хвилю RAG-ingestion подій до rag-ingest-worker, щоб агенти могли робити RAG по:
- чат-повідомленнях (
message.created), - документах/wiki (
doc.upserted), - файлах (
file.uploaded),
з урахуванням режимів public/confidential та прапору indexed.
Wave 1 = MVP RAG: максимум корисного контексту при мінімальній кількості подій.
Context
- Root:
microdao-daarion/. - Базовий воркер:
docs/cursor/rag_ingestion_worker_task.md. - Подробиці для перших подій:
docs/cursor/rag_ingestion_events_task.md(message/doc → IngestChunk). - Event Catalog:
docs/cursor/42_nats_event_streams_and_event_catalog.md. - Privacy/Confidential:
docs/cursor/47_messaging_channels_and_privacy_layers.mddocs/cursor/48_teams_access_control_and_confidential_mode.md
Ingestion-воркер читає події з NATS JetStream (streams типу STREAM_CHAT, STREAM_PROJECT, STREAM_TASK або teams.* outbox — згідно актуальної конфігурації).
1. Принципи для Wave 1
- Тільки доменні події, не CRUD по БД:
message.created,doc.upserted,file.uploaded.
- Поважати
modeтаindexed:- індексувати тільки якщо
indexed = true; - plaintext зберігати тільки для
public(дляconfidential— embeddings/summary без відкритого тексту, згідно політики).
- індексувати тільки якщо
- Мінімальний, але стандартний payload:
team_id,channel_idабоproject_id,mode(public | confidential),author_user_id/author_agent_id,created_at/updated_at,kind/doc_type,indexed(bool),source_ref(ID оригінальної сутності).
Ці принципи мають бути відображені як у схемах подій, так і в нормалізації → IngestChunk.
2. Event contracts (Wave 1)
2.1. message.created
Джерело: Messaging service (STREAM_CHAT / outbox для командних просторів).
Використати Event Envelope з 42_nats_event_streams_and_event_catalog.md, але уточнити payload для RAG:
- Subject/type (рекомендовано):
chat.message.created. - Envelope:
meta.team_id— DAO / команда.payload.message_id.payload.channel_id.payload.author_user_idабоpayload.author_agent_id.payload.mode:public | confidential.payload.kind:text | image | file | system.payload.thread_id(optional).payload.created_at.payload.indexed: bool (derived: mode + налаштування каналу).payload.text_summary/payload.text_plain(залежно від політики збереження plaintext).
RAG-правила:
- індексувати тільки якщо
payload.indexed = true; - якщо
kind != "text"— пропускати в Wave 1 (image/audio/pdf покриваються черезfile.uploaded); - якщо
mode = "confidential"— не зберігати plaintext в Milvus metadata, тільки embeddings + мінімальні метадані.
2.2. doc.upserted
Джерело: Docs/Wiki/Co-Memory сервіс (STREAM_PROJECT або окремий docs-stream).
Рекомендований payload для RAG:
payload.doc_idpayload.team_idpayload.project_idpayload.path(wiki path/tree)payload.titlepayload.text(може бути великий)payload.mode:public | confidentialpayload.indexed: boolpayload.labels/payload.tags(optional)payload.updated_at
RAG-правила:
- індексувати тільки якщо
indexed = true; - для великих текстів — розбивати на чанки (512–1024 символів/токенів);
mode = "confidential"→ embeddings без відкритого тексту.
2.3. file.uploaded
Джерело: Files/Co-Memory (files таблиця, окремий стрім або частина STREAM_PROJECT/STREAM_CHAT).
Рекомендований payload:
payload.file_idpayload.owner_team_idpayload.sizepayload.mimepayload.storage_keypayload.mode:public | confidentialpayload.indexed: boolpayload.enc: bool (чи зашифрований в storage)payload.linked_to:{message_id|project_id|doc_id}payload.extracted_text_ref(ключ до вже пропаршеного тексту, якщо є)
RAG-правила:
- індексувати тільки якщо
indexed = trueтаmime∈ текстових/документних форматів (text/*,application/pdf,markdown, тощо); - якщо текст ще не витягнутий — створити ingestion-джоб (черга/OCR) і не індексувати до появи
file.text_parsed/file.text_ready(це може бути окремий event у Wave 1 або 1.5).
3. Зміни в rag-ingest-worker
3.1. Routing / підписки
У services/rag-ingest-worker/events/consumer.py:
-
Додати (або уточнити) підписки на subjects для Wave 1:
chat.message.createddoc.upserted(назву узгодити з фактичним стрімом — напр.project.doc.upserted)file.uploaded
-
Ввести routing таблицю (може бути dict):
"chat.message.created" → handle_message_created"doc.upserted" → handle_doc_upserted"file.uploaded" → handle_file_uploaded
-
Кожен handler повинен:
- розпарсити envelope (
event,meta.team_id,payload), - перевірити
indexedтаmode, - викликати відповідну функцію нормалізації з
pipeline/normalization.py, - віддати chunks в embedding + Milvus + Neo4j.
- розпарсити envelope (
3.2. Нормалізація у pipeline/normalization.py
Розширити/уточнити:
-
async def normalize_message_created(event: dict) -> list[IngestChunk]:- орієнтуватися на схему з
rag_ingestion_events_task.md+ тепер додати перевіркуindexed/mode; - повертати 0 чанків, якщо
indexed = falseабоkind != "text".
- орієнтуватися на схему з
-
async def normalize_doc_upserted(event: dict) -> list[IngestChunk]:- аналогічно до
normalize_doc_upsertзrag_ingestion_events_task.md, але з полямиindexed,mode,labels; - розбивати довгі тексти.
- аналогічно до
-
async def normalize_file_uploaded(event: dict) -> list[IngestChunk]:- якщо текст уже доступний (через
extracted_text_refабо інший сервіс) — розбити на чанки; - якщо ні — поки що повертати
[]і логувати TODO (інтеграція з parser/Co-Memory).
- якщо текст уже доступний (через
У всіх нормалізаторах стежити, щоб:
chunk_idбув детермінованим (див.rag_ingestion_worker_task.md),visibility/modeкоректно мапились (public/confidential),source_type∈ {"message","doc","file"},- метадані включали
team_id,channel_id/project_id,author_id,created_at.
3.3. Embeddings + Milvus/Neo4j
У Wave 1 достатньо:
- використовувати вже існуючі пайплайни з
rag_ingestion_worker_task.md:embedding.embed_chunks(chunks)index_milvus.upsert_chunks_to_milvus(...)index_neo4j.update_graph_for_event(event, chunks)(мінімальний граф: User–Message–Channel, Project–Doc, File–(Message|Doc|Project)).
Головне — ідемпотентний upsert по chunk_id (Milvus) та MERGE в Neo4j.
4. Узгодження з Meilisearch indexer
Хоча цей таск фокусується на RAG (Milvus/Neo4j), потрібно:
- Переконатися, що логіка
indexed/modeспівпадає з існуючим search-indexer (Meilisearch) для:chat.message.created/chat.message.updated,doc.upserted,file.uploaded(якщо вже індексується).
- По можливості, винести спільну функцію/константу для визначення
indexed(based on channel/project settings), щоб RAG та Meilisearch не роз’їхались.
5. Тестування
Мінімальний набір тестів (unit/integration):
-
Unit:
normalize_message_created:indexed=false→[];kind != "text"→[];mode=public/indexed=true→ валідніIngestChunkз текстом;mode=confidential/indexed=true→ валідніIngestChunkбез plaintext у метаданих.
normalize_doc_upserted:- довгий текст → декілька чанків з коректними
chunk_id; indexed=false→[].
- довгий текст → декілька чанків з коректними
normalize_file_uploaded:- текст доступний → чанки;
- текст недоступний →
[]+ лог.
-
Integration (dev):
- опублікувати test-event
chat.message.createdу dev-стрім; - перевірити по логах, що воркер:
- спожив подію,
- зробив N чанків,
- відправив їх у embedding + Milvus;
- повторно відправити ту ж саму подію і переконатися, що дублікатів у Milvus немає.
- опублікувати test-event
Files to create/modify (suggested)
Актуальні шляхи можуть трохи відрізнятися — орієнтуйся по існуючому
rag-ingest-worker.
-
services/rag-ingest-worker/events/consumer.py- додати routing для
chat.message.created,doc.upserted,file.uploaded; - для кожної події — handler з перевіркою
indexed/modeта викликом нормалізатора.
- додати routing для
-
services/rag-ingest-worker/pipeline/normalization.py- реалізувати/оновити:
normalize_message_created(event)normalize_doc_upserted(event)normalize_file_uploaded(event)
- реалізувати/оновити:
-
(за потреби)
services/rag-ingest-worker/pipeline/index_neo4j.py- оновити побудову графових вузлів/ребер для Message/Doc/File.
-
Тести для нормалізаторів (якщо є тестовий пакет).
Acceptance criteria
-
rag-ingest-workerпідписаний на Wave 1 події (chat.message.created,doc.upserted,file.uploaded) у dev-конфігурації. -
Для кожної події є нормалізатор, який:
- поважає
modeтаindexed; - повертає коректні
IngestChunkз потрібними полями.
- поважає
-
Чанки успішно проходять через embedding-пайплайн і індексуються в Milvus з ідемпотентною семантикою (
chunk_id). -
Neo4j отримує хоча б базові вузли/ребра для Message/Doc/File.
-
Повторне програвання тих самих подій не створює дублікатів у Milvus/Neo4j.
-
Логіка
indexed/modeдля RAG узгоджена з Meilisearch search-indexer. -
Цей файл (
docs/cursor/rag_ingestion_events_wave1_mvp_task.md) можна виконати через Cursor:cursor task < docs/cursor/rag_ingestion_events_wave1_mvp_task.mdі Cursor використовує його як джерело правди для реалізації Wave 1 RAG-ingestion.