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
335 lines
12 KiB
Markdown
335 lines
12 KiB
Markdown
# 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) — PR1–PR3
|
||
|
||
### Архітектура
|
||
|
||
```
|
||
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
|