# Humanized Stepan — CHANGELOG v2.7 **Version:** v2.7 **Date:** 2026-02-25 **Базується на:** v2.6 (Jaccard guard, tone_constraints, 3-рівневі привітання, seeded RNG) --- ## Summary - Додано **memory horizon**: `recent_topics` (до 5 записів) замість єдиного `last_topic`. - Додано **human topic labels** (`last_topic_label`) — Степан оперує "план на завтра поле 12", а не "plan_day". - Додано **`summarize_topic_label()`** — rule-based витяг 6–8 слів з тексту без дієслів-тригерів і стоп-слів. - Light follow-up (≤6 слів + last_topic) **не додає шум** до `recent_topics` (`depth="light"` → `push` не відбувається). - Contextual greeting (`interaction_count ≥ 8`) тепер: з ймовірністю 20% (seeded rng) підхоплює `recent_topics[-2]` — Степан "пам'ятає" більше однієї теми без подвійного згадування. - **ZZR safety disclaimer**: якщо погодний тригер + обприскування/гербіцид/ЗЗР — автоматично додається `"Дозування та вікна застосування — за етикеткою препарату та регламентом."`. - Додано **`tests/test_stepan_invariants.py`** — 25 тестів-інваріантів проти "повзучої ботячості". --- ## Key features (деталі) ### Memory horizon — `recent_topics` ```json "recent_topics": [ {"label": "план на завтра поле 12", "intent": "plan_day", "ts": "2026-02-25T..."}, {"label": "датчики вологості поле 7", "intent": "iot_sensors", "ts": "2026-02-25T..."} ] ``` - Максимум 5 записів; старіші витісняються. - `last_topic` і `last_topic_label` — backward-compat aliases на `recent_topics[-1]`. - Dedup: якщо той самий `intent` + `label` підряд — не дублюється. ### summarize_topic_label | Вхід | Вихід | |---|---| | `"зроби план на завтра по полю 12"` | `"План на завтра по полю 12"` | | `"перевір датчики вологості поле 7"` | `"Датчики вологості поле 7"` | | `"сплануй тижневий збір по полях"` | `"Тижневий збір по полях"` | Правила: прибирається leading action verb (зроби/перевір/порахуй/…), стоп-слова, обрізка до 8 слів. Числа, поля, культури, дати зберігаються. ### ZZR disclaimer Regex `_ZZR_RE` спрацьовує на: `обробк|обприскування|гербіцид|фунгіцид|ЗЗР|пестицид|інсектицид|протруювач`. Застереження додається лише коли є **і** погодний тригер **і** ZZR-тригер в одному повідомленні. ### Invariant tests (anti-regression) | Інваріант | Обмеження | |---|---| | INV-1: Greeting | ≤ 80 символів | | INV-2: Thanks/Ack | ≤ 40 символів | | INV-3: Заборонені фрази | "чим можу допомогти", "оберіть", "я як агент", "я бот" | | INV-4: Технічні слова | container, uvicorn, trace_id, STEPAN_IMPORTS_OK | | INV-5: ZZR disclaimer | при ZZR+погода → "за етикеткою" або "за регламентом" | | INV-6: Horizon | `len(recent_topics) ≤ 5` після 7+ push | | INV-7: Міграція | lazy, idempotent, backward-compat | --- ## Backward compatibility | Аспект | Деталі | |---|---| | `_version` | 3 → 4 (нові поля `recent_topics`, `last_topic_label`) | | Міграція | Lazy при `load_user_profile()` — виконується автоматично при першому зверненні | | `last_topic` | Залишається як alias, завжди синхронізований з `recent_topics[-1].intent` | | `last_topic_label` | Новий alias на `recent_topics[-1].label`; якщо нема — встановлюється під час міграції | | `tone_constraints` | Вже в v2.6; міграція додає якщо відсутній | | `update_profile_if_needed` | Новий параметр `depth="deep"` (default) — backward-compat, старі виклики не ламаються | | `recent_topics` відсутній | Якщо профіль v3 без `recent_topics` — `migrate_profile_topics()` створює 1 елемент з `last_topic` | Міграція `migrate_profile_topics()` — **idempotent**: повторний виклик не змінює вже мігрований профіль. --- ## Non-goals / not included - Немає LLM у light mode або reflection. - Немає змін в інфраструктурі (Dockerfile, compose, env). - Немає змін у Gateway/http_api.py. - Немає нових API ендпоінтів. - Немає змін у поведінці deep mode orchestration. - Немає змін у системному промпті (тільки хедер-версія). --- ## Tests **Результат:** 101/101 зелених (без регресій з v2.6) | Файл | Тестів | Опис | |---|---|---| | `tests/test_stepan_invariants.py` | 25 | Нові інваріанти anti-regression | | `tests/test_stepan_acceptance.py` | 28 | Acceptance + v2.7 сесійні сценарії | | `tests/test_stepan_light_reply.py` | ~26 | Light reply юніт-тести | | `tests/test_stepan_memory_followup.py` | ~22 | Memory + follow-up класифікація | ```bash # Тільки інваріанти python3 -m pytest tests/test_stepan_invariants.py -v # Acceptance python3 -m pytest tests/test_stepan_acceptance.py -v # Всі Stepan тести python3 -m pytest tests/test_stepan_invariants.py tests/test_stepan_acceptance.py \ tests/test_stepan_light_reply.py tests/test_stepan_memory_followup.py -v ``` --- ## Known limitations ### Timezone і daily seed `date.today()` використовує локаль контейнера. Контейнер має бути в `Europe/Kyiv` (`TZ=Europe/Kyiv`), інакше "новий день" Степана настане о 22:00 або 23:00 за Київським часом. Перевірка: ```bash docker exec dagi-gateway-node1 date ``` ### Memory-service downtime При недоступності — деградація до локального in-memory кешу (TTL 30 хв). Кеш не переживає рестарт контейнера. Профілі не зберігаються між сесіями якщо memory-service down > 30 хв. ### ZZR regex — можливий overreach Слово `"обробка"` без агрохімічного контексту (напр. "обробка ґрунту") може спрацювати. Якщо в проді виявиться шум — звузити regex: вимагати ще одне слово з `[препарат|норма|л/га|кг/га|концентрат]`. --- ## Rollback ```bash # Відкатити зміни у конкретних файлах 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 gateway (без секретів) cd /opt/microdao-daarion docker compose -f docker-compose.node1.yml up -d --build dagi-gateway-node1 # Перевірка docker logs dagi-gateway-node1 --since 5m 2>&1 | grep -E "Stepan mode|STEPAN_IMPORTS_OK|error|Error" | tail -30 ```