# Voice Incidents Runbook **Version:** 1.0 | **Node:** NODA2 | **SLO doc:** `config/slo_policy.yml` --- ## Перший крок для БУДЬ-ЯКОГО алерту (30 секунд) ```bash # 1. Репро пакет — весь контекст в одному запиті curl -s http://localhost:8002/api/voice/degradation_status | python3 -m json.tool # 2. Canary живий синтез python3 ops/scripts/voice_canary.py --mode preflight --memory-url http://localhost:8000 # 3. Логи останніх 2 хвилин docker logs sofiia-console --since 2m 2>&1 | grep -E "ERROR|WARNING|TTS|LLM|502|429|503" docker logs dagi-memory-service-node2 --since 2m 2>&1 | grep -E "ERROR|403|edge.tts|synthesiz" ``` **Поля `repro` у відповіді** дають: `last_5_tts_errors`, `last_5_llm_errors`, `node_id`, `last_model`, `concurrent_tts_slots_free`. --- ## Alert 1: `VoiceTTFA_P95_Breach_Fast` **Умова:** TTFA p95 > 5000ms за 10 хвилин | **Severity:** warning **Що значить:** LLM відповідає повільно — черга Ollama переповнена, модель cold-start, або qwen3.5 вибрана замість gemma3. ### Крок 1 — Діагностика (2 хв) ```bash # Ollama поточний стан curl -s http://localhost:11434/api/ps | python3 -m json.tool # Метрики LLM по моделях (якщо є Prometheus) # promql: histogram_quantile(0.95, rate(voice_llm_ms_bucket[5m])) by (model) # Деградаційний стан curl -s http://localhost:8002/api/voice/degradation_status | python3 -c \ "import sys,json; d=json.load(sys.stdin); print(d['repro']['last_model'], d['p95'])" ``` ### Крок 2 — Mitigation ```bash # A. Примусово переключити на gemma3 (якщо qwen3.5 завантажений) # В UI: зняти галочку "Якісно" → fast profile автоматично обере gemma3 # B. Якщо Ollama завантажений запитами — зупинити важкі моделі curl -s -X POST http://localhost:11434/api/generate \ -d '{"model":"qwen3.5:35b-a3b","keep_alive":0}' # вивантажити з GPU # C. Якщо Ollama не відповідає — перезапуск docker restart ollama && sleep 10 curl -s http://localhost:11434/api/tags | python3 -m json.tool ``` ### Крок 3 — Verify ```bash python3 ops/scripts/voice_canary.py --mode runtime --memory-url http://localhost:8000 # Очікування: overall=ok, Polina/Ostap < 3000ms ``` --- ## Alert 2: `VoiceTTFA_P95_Breach_Quality` **Умова:** quality profile TTFA p95 > 7000ms | **Severity:** warning **Що значить:** qwen3.5 або qwen3:14b надто повільні. Часто — конкурентні запити або cold token generation. ### Дії 1. Перевірити `degradation_status.repro.last_model` — підтвердити що це quality profile. 2. Якщо це ізольована сесія — ігнорувати (quality SLO м'якший). 3. Якщо 5+ хвилин стабільно → переключити всіх на fast: в `router-config.yml` тимчасово видалити `voice_quality_uk` з `agent_voice_profiles.sofiia.quality_option`. 4. Після нормалізації — повернути. ```bash # Підтвердити що fast profile нормальний curl -s -X POST http://localhost:8002/api/voice/chat/stream \ -H "Content-Type: application/json" \ -d '{"message":"ping","model":"ollama:gemma3:latest","voice_profile":"voice_fast_uk"}' \ | python3 -c "import sys,json; d=json.load(sys.stdin); print('llm_ms:', d['meta']['llm_ms'])" ``` --- ## Alert 3: `VoiceQueueUnderflow_Spike` **Умова:** underflow rate > 1/хв за 5 хвилин | **Severity:** warning **Що значить:** браузер відтворює аудіо швидше ніж BFF синтезує `rest_chunks`. Користувач чує тишу між реченнями. ### Діагностика ```bash # Перевірити TTS latency (чи сповільнилось edge-tts?) curl -s http://localhost:8000/voice/health | python3 -c \ "import sys,json; d=json.load(sys.stdin); [print(v['voice'], v['ms'],'ms') for v in d['voices']]" # Перевірити concurrent TTS slots curl -s http://localhost:8002/api/voice/degradation_status | python3 -c \ "import sys,json; d=json.load(sys.stdin); print('free slots:', d['repro']['concurrent_tts_slots_free'])" ``` ### Mitigation - **Якщо TTS slow** (> 2s) → Alert 4 (edge-tts). Дивись нижче. - **Якщо concurrent slots = 0** → TTS DOS. Перевірити `docker stats dagi-memory-service-node2`. Збільшити `MAX_CONCURRENT_TTS` або перезапустити memory-service. - **Якщо slots OK** → перший чанк надто короткий (~1 речення). Тимчасове рішення — зменшити `MIN_CHUNK_CHARS` у `voice_utils.py` щоб більше тексту йшло у перший чанк. --- ## Alert 4: `VoiceTTS_P95_Degraded` **Умова:** TTS synthesis p95 > 2000ms за 10 хвилин | **Severity:** **critical** **Що значить:** edge-tts сповільнився або починає отримувати 403. Типова причина — Microsoft endpoint зміна auth або rate limiting. ### Крок 1 — Визначити тип помилки (1 хв) ```bash # Подивитись last_5_tts_errors curl -s http://localhost:8002/api/voice/degradation_status | python3 -c \ "import sys,json; d=json.load(sys.stdin); [print(e) for e in d['repro']['last_5_tts_errors']]" # Живий тест python3 ops/scripts/voice_canary.py --mode preflight --memory-url http://localhost:8000 ``` ### Якщо 403 errors: ```bash # Перевірити версію edge-tts docker exec dagi-memory-service-node2 pip show edge-tts | grep Version # Очікується: 7.2.7 # Якщо версія не 7.2.7 — оновити docker exec dagi-memory-service-node2 pip install edge-tts==7.2.7 docker restart dagi-memory-service-node2 sleep 10 && python3 ops/scripts/voice_canary.py --mode preflight ``` ### Якщо timeout / network: ```bash # Тест від сервера до Microsoft endpoint docker exec dagi-memory-service-node2 python3 -c \ "import asyncio, edge_tts; asyncio.run(edge_tts.list_voices())" # Якщо мережева проблема — тимчасово переключити на espeak (fallback) # В memory-service env: TTS_FALLBACK_ENGINE=espeak # Увага: якість значно гірша, але голос є ``` ### Нотувати в incident log: ```bash curl -s -X POST http://localhost:9102/v1/tools/execute \ -H "Content-Type: application/json" \ -d '{"tool":"oncall_tool","action":"incident_log_append","params":{"severity":"sev2","title":"TTS degraded — edge-tts","body":"VoiceTTS_P95_Degraded alert fired. Last errors: ..."}}' ``` --- ## Alert 5: `VoiceTTS_ErrorRate_High` **Умова:** TTS errors > 0.05/s за 3 хвилини | **Severity:** **critical** **Що значить:** масові відмови TTS синтезу. Користувачі або не чують нічого, або чують espeak-fallback. ### Перший крок (30 секунд) ```bash # Скільки помилок і якого типу docker logs dagi-memory-service-node2 --since 5m 2>&1 | grep -c "ERROR\|403\|edge.tts" docker logs dagi-memory-service-node2 --since 5m 2>&1 | grep "ERROR" | tail -5 ``` ### Mitigation tree: ``` error_type = 403 → Крок "Якщо 403 errors" з Alert 4 error_type = timeout → Перевірити мережу, перезапустити memory-service error_type = synthesis → pip install edge-tts==7.2.7 --force-reinstall error_type = OOM → docker stats → перезапустити memory-service з більшим RAM limit ``` ### Аварійний fallback (якщо нічого не допомогло): ```bash # Вимкнути автоспік у UI — щоб не показувало помилки # Або тимчасово вимкнути streaming docker exec sofiia-console env VOICE_STREAM_ENABLED=false \ uvicorn app.main:app --host 0.0.0.0 --port 8002 & # (не рекомендовано на prod без rebuild, але як аварійний захід) ``` ### Повідомити користувачів (якщо > 10 хвилин): - Додати banner у UI: змінна `VOICE_DEGRADED_BANNER` у env → відобразити через degradation badge "🔴 TTS DEGRADED" --- ## Escalation | Тривалість | Дія | |------------|-----| | < 10 хв | Автоматичний деградаційний badge у UI, моніторинг | | 10–30 хв | Mitigation з цього runbook, canary preflight | | > 30 хв | Escalate до @IvanTytar, записати incident в ops/incidents.jsonl | | > 2 год | Post-mortem draft (Sofiia-supervisor `postmortem_draft_graph`) | ```bash # Записати incident echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","sev":"sev2","title":"Voice TTS degraded","status":"open"}' \ >> ops/incidents.jsonl ``` --- ## Корисні команди (bookmark) ```bash # Швидкий статус всього voice стеку curl -s http://localhost:8002/api/voice/degradation_status | python3 -m json.tool curl -s http://localhost:8000/voice/health | python3 -c "import sys,json; d=json.load(sys.stdin); print('TTS:', d['edge_tts'], '| Polina:', [v for v in d['voices'] if 'Polina' in v['voice']][0]['ms'], 'ms')" python3 ops/scripts/voice_canary.py --mode preflight # Browser console для активних сесій # _voiceStats() — p50/p95 по останніх 20 турнах # _voice_degradation_sm — поточний стан на сервері # Prometheus queries (якщо є) # histogram_quantile(0.95, rate(voice_ttfa_ms_bucket[5m])) by (voice_profile) # rate(voice_tts_errors_total[5m]) # rate(voice_queue_underflows_total[5m]) * 60 ```