Files
microdao-daarion/docs/fabric_contract.md
Apple e9dedffa48 feat(production): sync all modified production files to git
Includes updates across gateway, router, node-worker, memory-service,
aurora-service, swapper, sofiia-console UI and node2 infrastructure:

- gateway-bot: Dockerfile, http_api.py, druid/aistalk prompts, doc_service
- services/router: main.py, router-config.yml, fabric_metrics, memory_retrieval,
  offload_client, prompt_builder
- services/node-worker: worker.py, main.py, config.py, fabric_metrics
- services/memory-service: Dockerfile, database.py, main.py, requirements
- services/aurora-service: main.py (+399), kling.py, quality_report.py
- services/swapper-service: main.py, swapper_config_node2.yaml
- services/sofiia-console: static/index.html (console UI update)
- config: agent_registry, crewai_agents/teams, router_agents
- ops/fabric_preflight.sh: updated preflight checks
- router-config.yml, docker-compose.node2.yml: infra updates
- docs: NODA1-AGENT-ARCHITECTURE, fabric_contract updated

Made-with: Cursor
2026-03-03 07:13:29 -08:00

335 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dev Contract: Preflight-first, Node-specific, Zero Assumptions (v0.1)
## 0. Заборона припущень
Будь-яка дія/пропозиція щодо моделей, провайдерів, портів, compose або routing **заборонена без preflight snapshot** з цільової ноди.
Без snapshot — ні коміт, ні деплой, ні рекомендація.
## 1. Обов'язковий Preflight Snapshot
### 1.1 Збір
Перед кожною зміною запустити `ops/fabric_snapshot.py` на цільовій ноді:
```bash
# NODA2 (локально)
python3 ops/fabric_snapshot.py --node-id NODA2
# NODA1 (через SSH tunnel)
ssh -L 18099:127.0.0.1:8099 -L 18109:127.0.0.1:8109 \
-L 19102:127.0.0.1:9102 -fN root@144.76.224.179
python3 ops/fabric_snapshot.py --node-id noda1 \
--ncs http://127.0.0.1:18099 --worker http://127.0.0.1:18109 \
--router http://127.0.0.1:19102 --ollama http://127.0.0.1:21434 \
--ssh root@144.76.224.179
```
Snapshot зберігається в `ops/preflight_snapshots/<node_id>_<timestamp>.json`.
### 1.2 Endpoints (всі обов'язкові)
| Endpoint | Що перевіряє |
|---|---|
| `NCS /capabilities` | served_models, capabilities, node_load, runtimes |
| `NCS /capabilities/caps` | capability flags (stt/tts/ocr/image) |
| `NCS /capabilities/installed` | installed_artifacts (disk scan) |
| `node-worker /caps` | provider flags (STT_PROVIDER, TTS_PROVIDER...) |
| `node-worker /healthz` | NATS connection, concurrency |
| `Router /v1/capabilities` | global view (всі ноди, capabilities_by_node) |
| `Router /v1/models` | глобальний пул моделей |
| `Ollama /api/tags` | реальні Ollama моделі на ноді |
| `docker ps` | список контейнерів |
### 1.3 Quality gate (must-pass)
Snapshot валідний тільки якщо:
- Router healthy
- NCS healthy
- Node-worker healthy
- `capabilities_by_node` містить цільову ноду
- `served_models` не порожній (або є пояснення "compute-less node")
## 2. Served ≠ Installed
### 2.1 Два шари правди
| Шар | Джерело | Використання |
|---|---|---|
| **served_models** | NCS /capabilities → runtimes (Ollama/llama-server/...) | Routing, model selection, offload |
| **installed_artifacts** | NCS /capabilities/installed → disk scan | Інвентаризація, recommendations, cleanup |
Модель на диску — це **candidate**, не "доступна".
### 2.2 Заборона hardcode
- Заборонено комітити `models:` списки в swapper/router configs
- Дозволено: policy-only `prefer` (тип/клас моделі), але не імена, крім критичних cloud SKU
## 3. Capability-first routing
### 3.1 Routing rules
Router обирає **ноду**, не "модель":
1. `find_nodes_with_capability(cap)` — тільки ноди де cap=true
2. `require_fresh_caps(ttl=30)` — preflight guard
3. Circuit breaker — виключити ноди з відкритим breaker
4. Load scoring — `inflight * 10 + (100 if mem_pressure=high)`
5. Offload через NATS `node.{id}.{cap}.request`
### 3.2 Fail fast
Якщо capability відсутня на всіх нодах — **fail fast** з явним повідомленням:
```json
{"error": "No node with capability 'stt' available", "capabilities_by_node": {...}}
```
Заборонено: тихий fallback на cloud без WARNING log.
### 3.3 Нодозалежність
STT/TTS/OCR/Image **можуть бути різними** на різних нодах:
- NODA2: `STT_PROVIDER=mlx_whisper`, `TTS_PROVIDER=mlx_kokoro`
- NODA1: `STT_PROVIDER=none`, `OCR_PROVIDER=vision_prompted`
Вмикання capability = тільки через env flags в node-worker → `/caps`.
## 4. Безпечний контроль змін
### 4.1 План змін (обов'язково)
Перед зміною відповісти на:
1. **Що** міняємо
2. **Чому**
3. **Що може зламатися**
4. **Як перевіряємо** (postflight)
5. **Rollback** (точна команда)
### 4.2 Rollback
Кожна зміна має мати:
- git commit hash / tag
- одну команду rollback (`docker compose up -d --force-recreate <service>`, `git checkout`)
## 5. Postflight
Після кожної зміни — повторний snapshot і порівняння:
- served_models count (не зменшився без причини)
- capabilities map (нові = очікувані)
- container count
- error rate (prom_metrics)
## 6. Жодних прихованих fallback
- Невідомий профіль або відсутній API key → WARNING + deterministic fallback на `agent.fallback_llm`
- Заборонено: "мовчки пішли в DeepSeek" без логу
## 7. Канонічні артефакти
| Артефакт | Призначення |
|---|---|
| `ops/fabric_snapshot.py` | Збір повного snapshot |
| `ops/fabric_preflight.sh` | Quick check + snapshot save |
| `ops/preflight_snapshots/` | Зберігання snapshots |
| `docs/fabric_contract.md` | Цей контракт |
## Реальний стан (snapshot 2026-02-27)
### NODA1 (production)
- **49 контейнерів** (gateway, router, memory, qdrant, neo4j, redis, postgres, minio, rag, swapper, farmos, brand-*, oneok-*, plant-vision, crawl4ai, grafana, prometheus...)
- **5 Ollama моделей**: qwen3-vl:8b (vision), qwen3:8b, qwen3.5:27b-q4_K_M, smollm2:135m, deepseek-v3.1:671b-cloud
- **14 Telegram агентів** active
- **NCS P3.5 не задеплоєний** — capabilities flags відсутні, installed_artifacts = 0
- `swapper=disabled`, worker NATS connected
### NODA2 (development)
- **14 контейнерів** (router, node-worker, node-capabilities, nats, gateway, memory, qdrant, postgres, neo4j, redis, open-webui, sofiia-console, swapper)
- **13 served моделей** (Ollama: 12 + llama_server: 1)
- **29 installed artifacts** на диску (150.3GB LLM + 0.3GB TTS kokoro-v1_0)
- **capabilities**: llm=Y, vision=Y, ocr=Y, stt=Y, tts=Y, image=N ← Phase 1 enabled
- `STT_PROVIDER=memory_service`, `TTS_PROVIDER=memory_service`, `OCR_PROVIDER=vision_prompted`
---
## Phase 1: STT/TTS via Memory Service delegation (2026-02-27)
### Мотивація
Увімкнення `stt=true` / `tts=true` в Fabric без нових мікросервісів і без ризику MLX-залежностей.
### Архітектура
```
Fabric Router → find_nodes_with_capability("stt"/"tts") → NODA2 node-worker
→ STT_PROVIDER=memory_service → stt_memory_service.transcribe()
→ POST http://memory-service:8000/voice/stt (faster-whisper)
→ {text, segments, language, meta}
Fabric Router → NODA2 node-worker
→ TTS_PROVIDER=memory_service → tts_memory_service.synthesize()
→ POST http://memory-service:8000/voice/tts (edge-tts: Polina/Ostap Neural uk-UA)
→ {audio_b64, format="mp3", meta}
```
### Контракти
**STT вхід:**
```json
{
"audio_b64": "<base64>", // OR
"audio_url": "http://...", // one is required
"language": "uk", // optional
"filename": "audio.wav" // optional
}
```
**STT вихід (fabric contract):**
```json
{"text": "...", "segments": [], "language": "uk", "meta": {...}, "provider": "memory_service"}
```
**TTS вхід:**
```json
{"text": "...", "voice": "Polina", "speed": 1.0}
```
**TTS вихід (fabric contract):**
```json
{"audio_b64": "<base64-mp3>", "format": "mp3", "meta": {...}, "provider": "memory_service"}
```
### Обмеження Phase 1
- **ffmpeg=false**: лише формати що Memory Service ковтає нативно (WAV рекомендований)
- **Текст TTS**: max 500 символів (Memory Service limit)
- **Голоси TTS**: Polina (uk-UA-PolinaNeural), Ostap (uk-UA-OstapNeural), en-US-GuyNeural
- **NODA1**: залишається `STT_PROVIDER=none` / `TTS_PROVIDER=none` (не заважає роутингу)
### Phase 2 (MLX upgrade — опційний)
Встановити `STT_PROVIDER=mlx_whisper` та/або `TTS_PROVIDER=mlx_kokoro` в docker-compose коли:
- готовий ffmpeg або чітко обмежені формати
- потрібний якісніший локальний TTS замість edge-tts
- NODA2 Apple Silicon виграш від MLX
---
## Voice HA (Multi-node routing) — PR1PR3
### Архітектура
```
Browser → sofiia-console /api/voice/tts
↓ VOICE_HA_ENABLED=false (default)
memory-service:8000/voice/tts ← legacy direct
↓ VOICE_HA_ENABLED=true
Router /v1/capability/voice_tts
↓ (caps + scoring)
node.{id}.voice.tts.request (NATS)
node-worker (voice semaphore)
memory-service/voice/tts
```
### NATS Subjects (Voice HA — відокремлені від generic)
| Subject | Призначення |
|---|---|
| `node.{id}.voice.tts.request` | Voice TTS offload (окремий semaphore) |
| `node.{id}.voice.llm.request` | Voice LLM inference (голосові guardrails) |
| `node.{id}.voice.stt.request` | Voice STT transcription |
**Сумісність:** generic subjects (`node.{id}.tts.request` etc.) — незмінні.
### Capability Flags
Node Worker `/caps` повертає:
```json
{
"capabilities": {
"tts": true,
"voice_tts": true,
"voice_llm": true,
"voice_stt": true
},
"voice_concurrency": {
"voice_tts": 4,
"voice_llm": 2,
"voice_stt": 2
}
}
```
`voice_tts=true` лише коли `TTS_PROVIDER != none` **і** NATS subscription активна.
NCS агрегує ці флаги через `_derive_capabilities()`.
### Router Endpoints
| Endpoint | Дедлайн | Суб'єкт |
|---|---|---|
| `POST /v1/capability/voice_tts` | 3000ms | `node.{id}.voice.tts.request` |
| `POST /v1/capability/voice_llm` | 9000ms (fast) / 12000ms (quality) | `node.{id}.voice.llm.request` |
| `POST /v1/capability/voice_stt` | 6000ms | `node.{id}.voice.stt.request` |
Response headers: `X-Voice-Node`, `X-Voice-Mode` (local|remote), `X-Voice-Cap`.
### Scoring
```
score = wait_ms + rtt_ms + p95_ms + mem_penalty - local_bonus
mem_penalty = 300 if mem_pressure == "high"
local_bonus = VOICE_PREFER_LOCAL_BONUS (default 200ms)
```
Якщо `score_local <= score_best_remote + LOCAL_THRESHOLD_MS` → вибирається локальна нода.
### BFF Feature Flag
```yaml
# docker-compose.node2-sofiia.yml
VOICE_HA_ENABLED: "false" # default — legacy direct path
VOICE_HA_ROUTER_URL: "http://router:8000" # Router для HA offload
```
Активація: `VOICE_HA_ENABLED=true` + rebuild `sofiia-console`.
Деактивація: `VOICE_HA_ENABLED=false` — повертається до direct memory-service.
### Метрики (Prometheus)
**node-worker** (`/prom_metrics`):
- `node_worker_voice_jobs_total{cap,status}`
- `node_worker_voice_inflight{cap}`
- `node_worker_voice_latency_ms{cap}` (histogram)
**router** (`/fabric_metrics`):
- `fabric_voice_capability_requests_total{cap,status}`
- `fabric_voice_offload_total{cap,node,status}`
- `fabric_voice_breaker_state{cap,node}` (1=open)
- `fabric_voice_score_ms{cap}` (histogram)
### Контракт: No Silent Fallback
- Будь-який fallback (busy, broken, timeout) логує `WARNING` + інкрементує Prometheus counter
- `TOO_BUSY` включає `retry_after_ms` hint для Router failover
- Circuit breaker per `node+voice_cap` — не змішується з generic CB
### Тести
`tests/test_voice_ha.py` — 28 тестів:
- Node Worker voice caps + semaphore isolation
- Router fabric_metrics voice helpers
- BFF `VOICE_HA_ENABLED` feature flag
- Voice scoring logic (local prefer, mem penalty, remote wins when saturated)
- No silent fallback invariants