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
466 lines
22 KiB
Markdown
466 lines
22 KiB
Markdown
# 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: Подяка — коротко, без питань
|
||
|
||
**Повідомлення:** `Дякую`
|
||
|
||
**Очікування:**
|
||
- Відповідь: 2–5 слів, ≤ 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.60–0.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
|
||
```
|
||
|
||
**Норма:** 20–40% від усіх 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.60–0.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
|
||
```
|