# 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 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'').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 ```