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
22 KiB
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-тестуванням перевірити:
# 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 у логах:
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 у логах:
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 у логах:
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 у логах:
docker logs dagi-gateway-node1 --since 2m 2>&1 | grep -E "depth=light|weather|crew_launch=false"
Сценарій 5: Подяка — коротко, без питань
Повідомлення: Дякую
Очікування:
- Відповідь: 2–5 слів, ≤ 40 символів
- Без питань
- Без "будь ласка, звертайтесь", без довгих формулювань
Grep у логах:
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 (уніфіковані)
# ─── Усі метричні рядки Степана ─────────────────────────────────────────────
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.
# ─── 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):
# Знайти псевдонім вручну (виконати разом з оператором):
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-відповіді.
Умови (всі одночасно):
depth == "deep"reflection.confidence >= 0.7(або reflection відсутній)interaction_count % 10 == 0- В
known_intentsодин intent зустрівся ≥ 3 рази - НЕ (
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:
docker ps | grep memory-service
docker logs memory-service --since 10m 2>&1 | tail -30
Дивна повторюваність відповідей між днями
Симптом: Степан відповідає однаково кілька днів підряд (не змінюється щодня)
Причина: TZ контейнера — UTC замість Europe/Kyiv; date.today() повертає UTC-дату
Дія:
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
Дія:
# Перевірити 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 — додати вимогу другого слова:
# Поточний: r'\b(обробк|обприскування|...)\w*\b'
# Звужений: вимагати [препарат|норма|л/га|кг/га] поруч
Це зміна в light_reply.py — перед внесенням перезапустити test_stepan_invariants.py::test_inv5_*.
Степан не відповідає (Stepan disabled)
Симптом: у логах Stepan disabled або STEPAN_IMPORTS_OK=False
Дія:
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 (тільки код)
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
# Якщо збережений попередній 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 — мігрується при першому запиті |
Як перевірити що міграція відбулась
docker logs dagi-gateway-node1 --since 60m 2>&1 \
| grep "AGX_STEPAN_METRIC farm_profile_migrated"
Як виявити конфлікт
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-відповідей:
# За останню годину
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 зріс після деплою — першою дією є:
python3 -m pytest tests/test_stepan_invariants.py -v