# 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
```
