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

22 KiB
Raw Permalink Blame History

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: Подяка — коротко, без питань

Повідомлення: Дякую

Очікування:

  • Відповідь: 25 слів, ≤ 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.600.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

Норма: 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:

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.600.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