Files
microdao-daarion/docs/HUMANIZED_STEPAN_v2.7_RUNBOOK.md
Apple 67225a39fa docs(platform): add policy configs, runbooks, ops scripts and platform documentation
Config policies (16 files): alert_routing, architecture_pressure, backlog,
cost_weights, data_governance, incident_escalation, incident_intelligence,
network_allowlist, nodes_registry, observability_sources, rbac_tools_matrix,
release_gate, risk_attribution, risk_policy, slo_policy, tool_limits, tools_rollout

Ops (22 files): Caddyfile, calendar compose, grafana voice dashboard,
deployments/incidents logs, runbooks for alerts/audit/backlog/incidents/sofiia/voice,
cron jobs, scripts (alert_triage, audit_cleanup, migrate_*, governance, schedule),
task_registry, voice alerts/ha/latency/policy

Docs (30+ files): HUMANIZED_STEPAN v2.7-v3 changelogs and runbooks,
NODA1/NODA2 status and setup, audit index and traces, backlog, incident,
supervisor, tools, voice, opencode, release, risk, aistalk, spacebot

Made-with: Cursor
2026-03-03 07:14:53 -08:00

466 lines
22 KiB
Markdown
Raw Permalink 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.
# Humanized Stepan — Production Runbook
**Version:** v3 (оновлено з v2.7)
**Date:** 2026-02-24
**Scope:** crews/agromatrix_crew (in-process Stepan, AGX_STEPAN_MODE=inproc)
---
## A) Purpose / Scope
Цей runbook описує операційний контроль Humanized Stepan (v2.7 → v3) у виробничому середовищі НОДА1.
Охоплює: перевірку справності, 5 smoke-сценаріїв, troubleshooting, rollback, v3 observability.
**Поза scope:** crewai-service HTTP mode (AGX_STEPAN_MODE=http), інші агенти.
---
## B) Preconditions
Перед smoke-тестуванням перевірити:
```bash
# 1. Степан увімкнений
docker exec dagi-gateway-node1 env | grep -E "AGX_STEPAN_MODE|STEPAN_IMPORTS_OK" | sed 's/=.*/=***/'
# 2. Оператор налаштований
docker exec dagi-gateway-node1 env | grep -E "AGX_OPERATOR_IDS|AGX_OPERATOR_CHAT_ID" | sed 's/=.*/=***/'
# 3. Memory-service доступний
docker exec dagi-gateway-node1 curl -s http://memory-service:8000/health | head -1
# 4. Timezone
docker exec dagi-gateway-node1 date
# Очікується: Europe/Kyiv або EET/EEST
# 5. Crews і tools на місці
docker exec dagi-gateway-node1 ls /app/crews/agromatrix_crew/ | head -5
docker exec dagi-gateway-node1 python3 -c "import agromatrix_tools; print('OK')"
```
---
## C) 5 Live Smoke Scenarios (Telegram)
Надсилаються оператором у чат, де активний Степан.
---
### Сценарій 1: Новий / невідомий user — Нейтральне привітання
**Повідомлення:** `Привіт`
**Очікування:**
- Відповідь: 1 коротка фраза, ≤ 80 символів
- Без "чим можу допомогти", без питання-списку
- Для першого звернення (interaction_count ≤ 2): нейтральна форма ("Привіт. Що зараз важливіше: план чи статуси?")
**Grep у логах:**
```bash
docker logs dagi-gateway-node1 --since 2m 2>&1 | grep -E "depth=light|crew_launch=false"
```
**Очікується:** `depth=light`, `crew_launch=false`
---
### Сценарій 2: Deep запит — тема записується в recent_topics
**Повідомлення:** `Зроби план на завтра по полю 12`
**Очікування:**
- Степан запускає orchestration (deep)
- Відповідь: план або уточнюючі питання
- `recent_topics` поповнюється записом типу `{"label": "план на завтра по полю 12", "intent": "plan_day", ...}`
**Grep у логах:**
```bash
docker logs dagi-gateway-node1 --since 2m 2>&1 | grep -E "depth=deep|crew_launch=true|topics_push=true"
```
**Очікується:** `depth=deep`, `crew_launch=true`, `topics_push=true`
---
### Сценарій 3: Light follow-up — тема НЕ додається повторно
**Повідомлення:** `а на післязавтра?` (одразу після сценарію 2)
**Очікування:**
- Відповідь: коротка, підхоплює тему ("план на завтра по полю 12" або подібне)
- `recent_topics` не змінюється (no new push)
- Crew не запускається
- **v3:** якщо сценарій 2 був light — `stability_guard_triggered` в логах замість стандартної класифікації
**Grep у логах:**
```bash
docker logs dagi-gateway-node1 --since 2m 2>&1 | grep -E "depth=light|topics_push=false|crew_launch=false|stability_guard_triggered"
```
**Очікується:** `depth=light`, `topics_push=false`, `crew_launch=false`
---
### Сценарій 4: Weather + ZZR — disclaimer обов'язковий
**Повідомлення:** `обприскування гербіцидом — якщо дощ сьогодні?`
**Очікування:**
- Відповідь містить практичну пораду по погоді (light mode)
- Відповідь **обов'язково** містить: `"за етикеткою"` або `"за регламентом"`
- Crew не запускається
**Grep у логах:**
```bash
docker logs dagi-gateway-node1 --since 2m 2>&1 | grep -E "depth=light|weather|crew_launch=false"
```
---
### Сценарій 5: Подяка — коротко, без питань
**Повідомлення:** `Дякую`
**Очікування:**
- Відповідь: 25 слів, ≤ 40 символів
- Без питань
- Без "будь ласка, звертайтесь", без довгих формулювань
**Grep у логах:**
```bash
docker logs dagi-gateway-node1 --since 2m 2>&1 | grep -E "depth=light|crew_launch=false"
```
---
## D) Telemetry Tag і Log Grep Patterns
### Telemetry Tag (v2.7.1)
Усі ключові метричні рядки мають єдиний префікс **`AGX_STEPAN_METRIC`**.
Формат: `AGX_STEPAN_METRIC <event> key=value key2=value2`
| Event | Ключі | Де генерується |
|---|---|---|
| `depth` | `depth=light\|deep reason=...` | `depth_classifier.py` |
| `crew_launch` | `launched=true\|false depth=...` | `run.py` |
| `topics_push` | `pushed=true\|false intent=... label=... horizon=N` | `memory_manager.py` |
| `memory_save` | `entity=UserProfile\|FarmProfile ok=true` | `memory_manager.py` |
| `memory_fallback` | `entity=... reason=memory_service_unavailable` | `memory_manager.py` |
| `memory_summary_updated` | `user_id=...` | `memory_manager.py` |
| `reflection_done` | `confidence=0.NN clarifying=true\|false new_facts=[...]` | `reflection_engine.py` |
| `reflection_skip` | `reason=recursion_guard\|error` | `reflection_engine.py` |
| `session_loaded` | `chat_id=h:... status=new\|hit last_depth=...` | `session_context.py` |
| `session_expired` | `chat_id=h:... age_s=N` | `session_context.py` |
| `session_updated` | `chat_id=h:... depth=... agents=[...]` | `session_context.py` |
| `stability_guard_triggered` | `chat_id=n/a words=N last_depth=light` | `depth_classifier.py` |
| `proactivity_added` | `user_id=h:... intent=... style=...` | `proactivity.py` |
| `proactivity_skipped` | `user_id=h:... reason=...` | `proactivity.py` |
### Grep one-liners (уніфіковані)
```bash
# ─── Усі метричні рядки Степана ─────────────────────────────────────────────
docker logs dagi-gateway-node1 --since 30m 2>&1 \
| grep "AGX_STEPAN_METRIC" | tail -50
# ─── Тільки depth (класифікація режиму) ─────────────────────────────────────
docker logs dagi-gateway-node1 --since 30m 2>&1 \
| grep "AGX_STEPAN_METRIC depth"
# ─── Тільки crew_launch ──────────────────────────────────────────────────────
docker logs dagi-gateway-node1 --since 30m 2>&1 \
| grep "AGX_STEPAN_METRIC crew_launch"
# ─── Тільки topics_push ──────────────────────────────────────────────────────
docker logs dagi-gateway-node1 --since 30m 2>&1 \
| grep "AGX_STEPAN_METRIC topics_push"
# ─── Memory fallback (аларм) ─────────────────────────────────────────────────
docker logs dagi-gateway-node1 --since 30m 2>&1 \
| grep "AGX_STEPAN_METRIC memory_fallback"
# ─── light_rate (тільки tagged рядки) ────────────────────────────────────────
L=$(docker logs dagi-gateway-node1 --since 60m 2>&1 \
| grep "AGX_STEPAN_METRIC depth" | grep -c "depth=light")
D=$(docker logs dagi-gateway-node1 --since 60m 2>&1 \
| grep "AGX_STEPAN_METRIC depth" | grep -c "depth=deep")
T=$((L + D))
if [ "$T" -ge 10 ]; then
echo "light=$L deep=$D total=$T light_rate=$(echo "scale=2; $L/$T" | bc)"
else
echo "light=$L deep=$D total=$T — замало даних (< 10), не робити висновків"
fi
```
**Норма light_rate:** 0.600.80 для типового оператора.
Нижче 0.50 → перевірити `_DEEP_ACTION_RE` у `depth_classifier.py` + запустити `test_stepan_invariants.py`.
```bash
# ─── v3: Session events (сесійний шар) ───────────────────────────────────────
docker logs dagi-gateway-node1 --since 2h 2>&1 \
| grep "AGX_STEPAN_METRIC session_" | tail -80
# ─── v3: Stability guard ─────────────────────────────────────────────────────
docker logs dagi-gateway-node1 --since 2h 2>&1 \
| grep "AGX_STEPAN_METRIC stability_guard_triggered" | tail -50
# ─── v3: Proactivity ─────────────────────────────────────────────────────────
docker logs dagi-gateway-node1 --since 2h 2>&1 \
| grep "AGX_STEPAN_METRIC proactivity_added" | tail -50
```
---
## E) PII-safe Telemetry (v2.7.2)
### Що анонімізується
Ключі `user_id` і `chat_id` у будь-якому `tlog()` виклику **автоматично** замінюються на хеш-псевдонім формату `h:<10 hex символів>`:
```
AGX_STEPAN_METRIC memory_save entity=UserProfile user_id=h:3f9a12b4c7 ok=true
```
Сирі ідентифікатори у `AGX_STEPAN_METRIC` рядках **відсутні**.
### Формат псевдоніму
```
h: + sha256(raw_id)[:10] → "h:3f9a12b4c7"
```
Завжди 12 символів. Стабільний для одного `user_id` між рестартами та логами.
### Кореляція подій одного користувача
Щоб знайти всі події одного користувача у логах (не знаючи сирого id):
```bash
# Знайти псевдонім вручну (виконати разом з оператором):
python3 -c "import hashlib; print('h:' + hashlib.sha256(b'<raw_user_id>').hexdigest()[:10])"
# Потім grep:
docker logs dagi-gateway-node1 --since 60m 2>&1 \
| grep "AGX_STEPAN_METRIC" | grep "h:3f9a12b4c7"
```
### Важливі застереження
- Це **не** криптографічна анонімізація. Якщо атакуючий знає `user_id` — він може відновити псевдонім і знайти події.
- Захищає від **випадкового** витоку у лог-агрегаторах (Loki, ELK, CloudWatch), де до логів мають доступ більше людей, ніж до БД.
- **Доступ до логів контейнера** має бути обмежений тільки для DevOps/операторів.
- Якщо потрібна повна GDPR/DPIA відповідність — застосуйте окрему маскування перед відправкою в зовнішній лог-сервіс.
---
## K) v3 Additions — Session / Proactivity / Stability Guard
### K1) Session Context Layer
**Що це:** in-memory кеш сесії на `chat_id`, TTL 15 хвилин.
**Зберігає:**
- `last_messages` (до 3 повідомлень)
- `last_depth` (`"light"` / `"deep"`)
- `last_agents` (до 5 назв агентів)
- `last_question` — уточнюючий запит від reflection, якщо був
**Важливо:**
- Сесія **не** пишеться у memory-service — тільки в оперативній пам'яті процесу.
- При рестарті контейнера сесія скидається — це очікувано (TTL 15 хв).
- При `session_expired` стан повертається в default без втрати профілів.
**Telemetry:**
```
AGX_STEPAN_METRIC session_loaded chat_id=h:... status=new|hit
AGX_STEPAN_METRIC session_expired chat_id=h:... age_s=N
AGX_STEPAN_METRIC session_updated chat_id=h:... depth=... agents=[...]
```
**Норма `session_expired`:** поодинокі. Якщо > 20/год на активному чаті — перевірити системний час контейнера (`docker exec dagi-gateway-node1 date`). Можлива причина: контейнер в UTC, а TZ операторів — Europe/Kyiv.
---
### K2) Intent Stability Guard
**Що це:** короткий follow-up після light-взаємодії не може випадково потрапити в deep.
**Умови спрацювання (всі одночасно):**
- `session.last_depth == "light"`
- Кількість слів ≤ 6
- Немає action verbs (`_DEEP_ACTION_RE`)
- Немає urgent слів (`_DEEP_URGENT_RE`)
**Перебивається:** будь-яке action verb або urgent слово — guard не спрацьовує і класифікація йде звичайним шляхом.
**Telemetry:**
```
AGX_STEPAN_METRIC stability_guard_triggered chat_id=n/a words=N last_depth=light
```
**Норма:** 2040% від усіх light-повідомлень після активної сесії — це нормально.
**Аларм:** якщо `stability_guard_triggered` домінує (> 90% від depth events) і deep майже зник — guard надто агресивний. Розслідувати, чи немає регресії у action verb regex.
---
### K3) Soft Proactivity Layer
**Що це:** рівно 1 коротке речення ≤ 120 символів, без `!`, додається в кінець deep-відповіді.
**Умови (всі одночасно):**
1. `depth == "deep"`
2. `reflection.confidence >= 0.7` (або reflection відсутній)
3. `interaction_count % 10 == 0`
4. В `known_intents` один intent зустрівся ≥ 3 рази
5. НЕ (`preferred_style == "brief"` AND відповідь вже містить `"?"`)
**Банки фраз:** 4 банки — generic, iot, plan, sustainability. Вибір seeded за `user_id + interaction_count`.
**Telemetry:**
```
AGX_STEPAN_METRIC proactivity_added user_id=h:... intent=... style=...
AGX_STEPAN_METRIC proactivity_skipped user_id=h:... reason=not_deep|not_tenth|...
```
**Норма:** рідко — 1 раз на ~10 deep-взаємодій з постійним користувачем. Якщо `proactivity_added` > 3 рази за 30 хв в одному чаті — перевірити `interaction_count` логіку.
---
## F) Troubleshooting
### Memory-service недоступний
**Симптом:** у логах `UserProfile fallback` або `memory.*timeout`
**Поведінка:** Степан продовжує роботу з in-memory кешем (TTL 30 хв). Профілі не зберігаються між рестартами.
**Дія:** перевірити memory-service:
```bash
docker ps | grep memory-service
docker logs memory-service --since 10m 2>&1 | tail -30
```
### Дивна повторюваність відповідей між днями
**Симптом:** Степан відповідає однаково кілька днів підряд (не змінюється щодня)
**Причина:** TZ контейнера — UTC замість Europe/Kyiv; `date.today()` повертає UTC-дату
**Дія:**
```bash
docker exec dagi-gateway-node1 date
# Якщо не Kyiv — додати в docker-compose.node1.yml:
# environment:
# TZ: "Europe/Kyiv"
```
### Занадто багато deep-запусків
**Симптом:** `crew_launch=true` на прості запити ("ок", "зрозумів")
**Причина:** регресія у action-verb regex або новий тригер у `_DEEP_ACTION_RE`
**Дія:**
```bash
# Перевірити depth_classifier.py — порівняти _DEEP_ACTION_RE з референсом v2.7
# Запустити інваріантні тести
python3 -m pytest tests/test_stepan_invariants.py tests/test_stepan_memory_followup.py -v
```
### ZZR disclaimer надто часто (false positives)
**Симптом:** "обробка ґрунту після дощу" отримує disclaimer
**Причина:** `_ZZR_RE` чіпляє загальне "обробк"
**Дія:** звузити regex — додати вимогу другого слова:
```python
# Поточний: r'\b(обробк|обприскування|...)\w*\b'
# Звужений: вимагати [препарат|норма|л/га|кг/га] поруч
```
Це зміна в `light_reply.py` — перед внесенням перезапустити `test_stepan_invariants.py::test_inv5_*`.
### Степан не відповідає (Stepan disabled)
**Симптом:** у логах `Stepan disabled` або `STEPAN_IMPORTS_OK=False`
**Дія:**
```bash
docker logs dagi-gateway-node1 --since 5m 2>&1 | grep -E "ImportError|ModuleNotFoundError|Stepan disabled"
# Якщо crews відсутні:
docker exec dagi-gateway-node1 ls /app/crews/agromatrix_crew/ | head -5
# Якщо agromatrix_tools відсутній:
docker exec dagi-gateway-node1 python3 -c "import agromatrix_tools"
```
---
## F) Safety Notes
### ZZR Disclaimer — чому він тут
Степан може надавати погодні рекомендації у light mode (без LLM, rule-based). Коли в запиті є обприскування/гербіцид + погодні умови, є ризик надто конкретної поради по нормам або вікнах застосування. Disclaimer фіксує відповідальність на етикетці препарату і є **mandatory** — не видаляти без перегляду safety policy.
### Seeded RNG — чому щоденна, а не per-interaction
Stабільність відповідей на рівні дня — це баланс між передбачуваністю та людяністю. Якщо seed per-interaction — фрази відчуваються "скачуть" у межах одної сесії. Якщо seed стала — фрази однакові тижнями. Daily seed дає природну варіацію без artifactів.
---
## G) Rollback Steps
### Швидкий rollback (тільки код)
```bash
cd /opt/microdao-daarion
# Відкатити Stepan-файли до попередньої версії
git checkout HEAD~1 -- crews/agromatrix_crew/memory_manager.py
git checkout HEAD~1 -- crews/agromatrix_crew/light_reply.py
git checkout HEAD~1 -- crews/agromatrix_crew/run.py
# Rebuild
docker compose -f docker-compose.node1.yml up -d --build dagi-gateway-node1
# Verify
docker logs dagi-gateway-node1 --since 3m 2>&1 | grep -E "Stepan mode|STEPAN_IMPORTS_OK" | tail -5
```
### Rollback через Docker image tag
```bash
# Якщо збережений попередній image tag (наприклад :v2.6)
docker compose -f docker-compose.node1.yml down dagi-gateway-node1
docker tag dagi-gateway-node1:v2.6 dagi-gateway-node1:current
docker compose -f docker-compose.node1.yml up -d dagi-gateway-node1
```
---
## H) Multi-user Farm Model (v2.8)
### Схема зберігання
| Що | Ключ | Хто ділить |
|---|---|---|
| UserProfile | `user_profile:agromatrix:{user_id}` | Тільки один user |
| FarmProfile | `farm_profile:agromatrix:chat:{chat_id}` | Усі users у чаті |
| FarmProfile (legacy) | `farm_profile:agromatrix:{user_id}` | Deprecated — мігрується при першому запиті |
### Як перевірити що міграція відбулась
```bash
docker logs dagi-gateway-node1 --since 60m 2>&1 \
| grep "AGX_STEPAN_METRIC farm_profile_migrated"
```
### Як виявити конфлікт
```bash
docker logs dagi-gateway-node1 --since 60m 2>&1 \
| grep "AGX_STEPAN_METRIC farm_profile_conflict"
```
При конфлікті — chat-profile **не** перезаписується. Лише лог. Якщо потрібно вирішити вручну — або очистити legacy ключ у memory-service, або видалити chat-ключ.
## J) Monitoring Suggestions (Manual)
**light_rate** — частка light-відповідей:
```bash
# За останню годину
L=$(docker logs dagi-gateway-node1 --since 60m 2>&1 | grep -c "depth=light")
D=$(docker logs dagi-gateway-node1 --since 60m 2>&1 | grep -c "depth=deep")
echo "light=$L deep=$D ratio=$(echo "scale=2; $L/($L+$D)" | bc)"
```
Норма: light_rate ≈ 0.600.80 для типового оператора. Нижче 0.50 — перевірити action-verb regex.
**avg_chars_light / avg_chars_deep** — вручну для вибірки:
Зберегти кілька реальних відповідей і підрахувати довжину. Light має бути < 120 символів у медіані.
Якщо light_rate різко знизився або avg_chars_light зріс після деплою — першою дією є:
```bash
python3 -m pytest tests/test_stepan_invariants.py -v
```