Files
microdao-daarion/docs/Humanized_Stepan_Architecture_Plan.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

691 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Humanized Stepan v2 — Architecture Plan
**Версія:** 0.1-draft
**Статус:** plan (без коду)
**Область змін:** `crews/agromatrix_crew/` + мінімальний торкання `http_api.py`
**Принцип:** fail-closed, backward-compatible, жодної нескінченної рекурсії
---
## 1. Проблеми поточної архітектури
| Симптом | Причина у коді |
|---------|----------------|
| На "привіт" запускаються всі 5 під-агентів | `run.py` завжди викликає ops, iot, platform, spreadsheet, sustainability |
| Роботизовані відповіді | JSON-схема фінального агента, відсутня адаптація стилю |
| Степан не знає хто ти | Немає UserProfile, жодного звернення до memory-service |
| Степан не знає твою ферму | Немає FarmProfile |
| Після відповіді немає самоперевірки | Reflection відсутній |
| Оператор і звичайний користувач мають однакову відповідь | is_operator є, але стиль не змінюється |
| Зміна `detect_intent()` ламає всю логіку | Ключові слова захардкожені в одній функції |
---
## 2. Загальна схема нового потоку
```
handle_message(text, user_id, chat_id, ops_mode)
├─► [activation_gate.pre_check(text)] ← блокує рекурсію, лічить глибину
├─► [memory_manager.load(user_id)] ← UserProfile + FarmProfile
│ │ fallback: порожній профіль ← fail-safe
├─► [depth_classifier.classify(text, profile)]
│ │ → DepthDecision {mode, intent, crew_needed, confidence}
│ │ fallback: mode="deep" ← fail-closed: краще зробити більше
├─► if mode == "light":
│ [style_adapter.render(profile)] → system_prompt_prefix
│ Stepan відповідає сам (без під-агентів)
│ → response
├─► if mode == "deep":
│ [activation_gate.select_crew(DepthDecision, FarmProfile)]
│ → {ops?, iot?, platform?, spreadsheet?, sustainability?}
│ Запускати ТІЛЬКИ потрібних під-агентів
│ Stepan консолідує
│ → response
├─► [reflection_engine.reflect(response, profile, intent)] ← один прохід, не рекурсія
│ │ fallback: оригінальна відповідь
├─► [memory_manager.update_async(user_id, text, response)] ← не блокує
└─► return final_response
```
---
## 3. Нові модулі
### 3.1 `depth_classifier.py`
**Розташування:** `crews/agromatrix_crew/depth_classifier.py`
**Відповідальність:** визначити глибину запиту і які под-агенти взагалі потрібні.
**Вхід:**
- `text: str` — текст повідомлення
- `profile: UserProfile | None` — профіль користувача
- `farm: FarmProfile | None` — профіль ферми
**Вихід: `DepthDecision`**
```python
@dataclass
class DepthDecision:
mode: Literal["light", "deep"] # ключовий перемикач
intent: str # human-readable intent
crew_needed: list[str] # підмножина: ops, iot, platform, spreadsheet, sustainability
confidence: float # 0..1, < 0.4 → force deep
reason: str # для audit логу
```
**Логіка класифікації (rule-based, без LLM):**
Light mode — якщо текст відповідає хоча б одному патерну:
```
LIGHT_PATTERNS = {
"greeting": ["привіт", "доброго", "hello", "hi", "добрий ранок", "добрий вечір"],
"thanks": ["дякую", "дякуй", "спасибі", "дякую степан"],
"ack": ["зрозумів", "ок", "добре", "чудово", "зрозуміла"],
"whoami_check": ["хто я", "мої права"],
"simple_status": ["який статус", "що зараз"],
}
```
Deep mode — якщо текст відповідає хоча б одному:
```
DEEP_PATTERNS = {
"planning": ["сплануй", "план на", "розробити план", "графік робіт"],
"multi_ops": ["по всіх полях", "кілька ділянок", "всі культури"],
"iot_alert": ["аномалія", "тривога", "sensors", "вологість впала"],
"analysis": ["план/факт", "план факт", "статистика", "зведення", "порівняй"],
"decision": ["що робити", "порадь", "проаналізуй", "виріши"],
"recording": ["запиши", "зафіксуй", "внеси", "додай операцію"],
}
```
Crew selection у deep mode:
```
crew_needed logic:
"ops" → "запиши" | "зафіксуй" | "внеси" | farmos keywords
"iot" → "датчик" | "вологість" | "temp" | "sensor" | FarmProfile.has_iot
"platform" → "статус сервісів" | "інтеграція" | "помилка підключення"
"spreadsheet" → "таблиц" | "excel" | "звіт" | "xlsx"
"sustainability" → "зведення" | "агрегація" | "підсумки"
```
**Fail-safe:** будь-який виняток → `DepthDecision(mode="deep", intent="unknown", crew_needed=["ops","iot","platform","spreadsheet","sustainability"], confidence=0.0, reason="classifier_error")`.
---
### 3.2 `memory_manager.py`
**Розташування:** `crews/agromatrix_crew/memory_manager.py`
**Відповідальність:** завантажити, зберегти і оновити профілі через memory-service. Повна деградація до in-memory fallback.
**API:**
```python
def load(user_id: str) -> tuple[UserProfile, FarmProfile]
def update(user_id: str, interaction: InteractionContext) -> None
```
**Реалізація (sync, бо `run.py` sync):**
- HTTP запити через `httpx.Client` (sync), timeout 2s
- При недоступності memory-service → використовує `_local_cache: dict` (процесна пам'ять)
- `_local_cache` зберігає до 200 записів, TTL 30 хвилин
- Факт-ключі в memory-service:
- `user_profile:agromatrix:{user_id}`
- `farm_profile:agromatrix:{user_id}`
- user_id для memory-service: `stepan:{user_id}` (ізоляція від gateway-агентів)
**Fail-safe:**
```python
try:
profile = _fetch_from_memory(user_id)
except Exception:
profile = UserProfile.default(user_id) # порожній, але валідний
logger.warning("memory_manager: fallback to default profile user=%s", user_id)
```
**Не блокуючий update:**
```python
def update_async(user_id: str, interaction: InteractionContext):
"""Запускає оновлення в threading.Thread (daemon=True), не чекає результату."""
t = threading.Thread(target=_do_update, args=(user_id, interaction), daemon=True)
t.start()
```
---
### 3.3 `style_adapter.py`
**Розташування:** `crews/agromatrix_crew/style_adapter.py`
**Відповідальність:** сформувати prefix для system prompt Степана залежно від профілю.
**Вхід:** `UserProfile`, `DepthDecision`
**Вихід:** `str` — prefix для system prompt Степана
**Рівні expertise:**
```
novice: мова проста, уникай термінів, давай короткий приклад, 2-3 речення
intermediate: збалансована відповідь, терміни пояснюй в дужках, до 5 речень
expert: технічна відповідь, скорочений формат, опускай очевидне
```
**Стилі:**
```
brief: 1-2 речення, тільки суть
detailed: повний опис з контекстом
conversational: живий тон, питання-відповідь, можна питати уточнення
```
**Формат prefix:**
```
"Відповідай на рівні {expertise_label}.
Стиль: {style_label}.
Ти знаєш цього користувача: {name or 'агрономе'}.
Фермерський контекст: {farm_context_summary}."
```
**Fail-safe:** будь-який виняток → повертає порожній рядок, Степан працює зі стандартним backstory.
---
### 3.4 `reflection_engine.py`
**Розташування:** `crews/agromatrix_crew/reflection_engine.py`
**Відповідальність:** одноразова пост-обробка відповіді для відповідності профілю і стилю.
**Механізм (без LLM для Light mode, з LLM для Deep mode):**
**Light mode reflection (rule-based):**
- Відповідь > 500 символів і UserProfile.preferred_style == "brief" → обрізати до 3 речень
- Відповідь містить JSON-фрагменти → замінити на людський текст
- Відповідь містить технічні ідентифікатори (uuid, trace_id) → прибрати з відповіді користувачу
**Deep mode reflection (LLM, one-shot):**
```
Prompt:
"Оціни цю відповідь для {expertise_level} користувача:
[RESPONSE]
Якщо відповідь занадто технічна — спрости.
Якщо занадто довга для {preferred_style} — скороти.
Відповідай тільки виправленою відповіддю."
```
**Anti-recursion guard:**
```python
# В reflection_engine.py — module-level flag
_REFLECTING: bool = False
def reflect(response: str, profile: UserProfile, trace_id: str) -> str:
global _REFLECTING
if _REFLECTING:
logger.warning("reflection: recursion guard active, skipping trace=%s", trace_id)
return response
_REFLECTING = True
try:
return _do_reflect(response, profile, trace_id)
except Exception:
return response
finally:
_REFLECTING = False
```
**Fail-safe:** будь-який виняток → повертає оригінальну відповідь без змін.
---
### 3.5 `activation_gate.py`
**Розташування:** `crews/agromatrix_crew/activation_gate.py`
**Відповідальність:**
1. Pre-check: блокує подвійний виклик handle_message з того самого контексту
2. Select: визначає мінімальний набір під-агентів для запуску
3. Post-check: обмежує глибину делегування
**Структура:**
```python
_CALL_DEPTH: threading.local # per-thread, не глобальне
MAX_DEPTH = 1 # Степан може делегувати, але не можна повторно входити в handle_message
def pre_check(trace_id: str) -> bool:
"""Повертає True якщо дозволено продовжувати, False якщо глибина перевищена."""
depth = getattr(_CALL_DEPTH, "depth", 0)
if depth >= MAX_DEPTH:
logger.error("activation_gate: max depth %d reached trace=%s", MAX_DEPTH, trace_id)
return False
_CALL_DEPTH.depth = depth + 1
return True
def release(trace_id: str):
"""Зменшити лічильник після завершення handle_message."""
_CALL_DEPTH.depth = max(0, getattr(_CALL_DEPTH, "depth", 0) - 1)
def select_crew(decision: DepthDecision, farm: FarmProfile) -> list[str]:
"""Повернути список під-агентів для запуску."""
needed = list(decision.crew_needed)
# Видалити IoT якщо FarmProfile.active_integrations не має iot
if "iot" in needed and not farm.has_iot_integration:
needed.remove("iot")
# Видалити spreadsheet якщо не запит до таблиць
if "spreadsheet" in needed and "spreadsheet" not in decision.intent:
needed.remove("spreadsheet")
return needed if needed else []
```
---
## 4. Структура UserProfile JSON
```json
{
"_version": 1,
"_fact_key": "user_profile:agromatrix:{user_id}",
"user_id": "tg:123456789",
"agent": "agromatrix",
"name": "Іван",
"expertise_level": "intermediate",
"preferred_language": "uk",
"preferred_style": "conversational",
"last_seen": "2026-02-24T10:00:00Z",
"interaction_count": 42,
"known_intents": [
"plan_day",
"show_critical_tomorrow",
"iot_status"
],
"context_notes": [
"has_farmos_access",
"uses_thingsboard",
"prefers_short_answers"
],
"farm_profile_ref": "farm_profile:agromatrix:{user_id}",
"recent_topics": [
{"intent": "plan_day", "ts": "2026-02-24T09:00:00Z"},
{"intent": "iot_status", "ts": "2026-02-23T18:00:00Z"}
],
"operator": false,
"updated_at": "2026-02-24T10:00:00Z"
}
```
**Поля та семантика:**
| Поле | Тип | Опис |
|------|-----|------|
| `expertise_level` | enum | novice / intermediate / expert; оновлюється автоматично після 10+ взаємодій |
| `preferred_style` | enum | brief / detailed / conversational |
| `interaction_count` | int | лічильник всіх взаємодій для авто-підвищення рівня |
| `known_intents` | list[str] | унікальні intents, накопичуються; use для FarmProfile автодоповнення |
| `context_notes` | list[str] | вільні мітки, збагачуються під час взаємодій |
| `recent_topics` | list[{intent, ts}] | останні 10 тем (для cold-start relief) |
| `operator` | bool | чи є цей user оператором (AGX_OPERATOR_IDS); read-only у memory |
---
## 5. Структура FarmProfile JSON
```json
{
"_version": 1,
"_fact_key": "farm_profile:agromatrix:{user_id}",
"user_id": "tg:123456789",
"farm_name": "Ферма Калинівка",
"field_ids": ["field:north-01", "field:south-02"],
"crop_ids": ["crop:wheat-winter", "crop:corn-hybrid"],
"active_integrations": ["farmos", "thingsboard"],
"seasonal_context": {
"current_phase": "growing",
"active_operations": ["irrigation", "monitoring"],
"hemisphere": "north",
"approximate_month": 2
},
"iot_sensors": {
"has_iot_integration": true,
"sensor_types": ["soil_moisture", "temperature"],
"last_alert": null
},
"typical_intents": ["plan_day", "iot_status", "plan_vs_fact"],
"alert_thresholds": {
"soil_moisture_min": 20.0,
"temperature_min": -5.0,
"temperature_max": 38.0
},
"dict_pending_count": 0,
"updated_at": "2026-02-24T10:00:00Z"
}
```
**Поля та семантика:**
| Поле | Тип | Опис |
|------|-----|------|
| `field_ids` | list[str] | заповнюються під час нормалізації терміну tool_dictionary |
| `crop_ids` | list[str] | аналогічно |
| `active_integrations` | list[str] | визначають які crew_agents потенційно потрібні |
| `seasonal_context` | object | підказки для планування і класифікатора глибини |
| `iot_sensors.has_iot_integration` | bool | ключ для activation_gate: чи включати IoT агента |
| `typical_intents` | list[str] | акумулюються; використовуються для Light/Deep розмежування |
| `dict_pending_count` | int | кеш кількості pending термінів для оператора |
| `alert_thresholds` | object | якщо IoT дані виходять за поріг → auto-trigger Deep mode |
---
## 6. Коли і як оновлюється профіль
### UserProfile
| Подія | Що оновлюється | Коли |
|-------|----------------|------|
| Будь-яка взаємодія | `last_seen`, `interaction_count`, `recent_topics` | Завжди, після відповіді |
| Новий intent | `known_intents.append(intent)` | Якщо intent не порожній |
| interaction_count >= 10 і всі intents — "planning" | `expertise_level` → intermediate | При update |
| interaction_count >= 30 і є технічні intents | `expertise_level` → expert | При update |
| Оператор надіслав `/profile set style brief` | `preferred_style` | Одразу |
| FarmProfile змінений | `farm_profile_ref` sync | При update |
### FarmProfile
| Подія | Що оновлюється | Коли |
|-------|----------------|------|
| tool_dictionary.normalize успішний | `field_ids`, `crop_ids` | При нормалізації |
| Новий інтент з IoT | `active_integrations`, `iot_sensors.has_iot_integration` | При Deep mode |
| Новий інтент з spreadsheet | `active_integrations.append("spreadsheet")` | При Deep mode |
| Оператор `/farm update phase=sowing` | `seasonal_context.current_phase` | Одразу |
| dict_review.stats() | `dict_pending_count` | При ops_mode load |
---
## 7. Тригери Deep mode
**Автоматичні (depth_classifier):**
| Тригер | Умова |
|--------|-------|
| Планування | текст містить DEEP_PATTERNS["planning"] |
| Мультипольова операція | DEEP_PATTERNS["multi_ops"] |
| IoT аномалія | DEEP_PATTERNS["iot_alert"] АБО IoT дані з alert_thresholds порушені |
| Аналіз план/факт | DEEP_PATTERNS["analysis"] |
| Запис у farmOS | DEEP_PATTERNS["recording"] |
| Низька впевненість | confidence < 0.4 після класифікації |
| Нові терміни | tool_dictionary normalization повернув pending items |
| Перша взаємодія | interaction_count == 0 (невідомий користувач) |
**Примусові (env/flag):**
| Тригер | Механізм |
|--------|----------|
| `AGX_FORCE_DEEP=1` | env в контейнері (тестування) |
| Текст починається з `--deep` | парситься в handle_message before classify |
| Оператор вручну | operator_commands + flag в trace |
---
## 8. Тригери запуску під-команди (активація crew_agent)
| Crew Agent | Тригер (keyword or FarmProfile) | Light може обійтись? |
|------------|----------------------------------|----------------------|
| `ops` | "запиши", "внеси", "зафіксуй", "farmOS" | Ні |
| `iot` | "датчик", "вологість", "температура" + `has_iot_integration=true` | Ні |
| `platform` | "статус", "перевір сервіс", "інтеграція впала" | Іноді (кешований статус) |
| `spreadsheet` | "таблиця", "excel", "звіт", "xlsx" | Ні |
| `sustainability` | "зведення", "агрегація", "підсумки по сезону" | Ні |
| **всі одночасно** | `intent == "general"` без профілю (fallback) | Ні |
---
## 9. Ситуації, що залишаються Light mode
| Ситуація | Чому Light | Хто відповідає |
|----------|------------|----------------|
| Привітання будь-якого типу | Не потребує даних з farmOS/IoT | Степан з style_adapter |
| "Дякую", "ок", "зрозумів" | Підтвердження, не запит | Степан (2 слова) |
| /whoami, /pending, /approve | Operator commands | operator_commands.py (незмінний) |
| "Що ти вмієш?" | Довідка | Степан з профілем |
| Повторне питання тієї ж теми (< 5 хв) | recent_topics cache | Степан з кешем контексту |
| Simple status якщо кеш свіжий | FarmProfile.seasonal_context свіжий (< 1 год) | Степан без crew |
| Повідомлення < 4 слів | Незрозумілий запит → уточнення | Степан питає |
| Текст не пов'язаний з агрономією | Off-topic filter | Степан ввічливо redirects |
---
## 10. Принцип fail-safe
**Ієрархія деградації:**
```
Нормальна робота:
memory-service online → профілі загружені → класифікатор → вибір crew → рефлексія
Деградація 1 (memory недоступна):
fallback UserProfile.default() → класифікатор без персоналізації → crew → рефлексія skip
Деградація 2 (classifier помилка):
force Deep mode → всі crew → рефлексія skip
Деградація 3 (частина crew агентів впала):
інші crew продовжують → Степан синтезує з частковими даними
run_task_with_retry вже існує (max_retries=2)
Деградація 4 (OpenAI недоступний):
handle_stepan_message повертає "Помилка обробки. trace_id=..."
gateway вже обробляє це (stepan_disabled fallback)
```
**Правила:**
- Жодний модуль не може кинути виняток, що зупинить `handle_message`
- Кожен новий модуль wrap-ується в try/except з fallback
- `reflection_engine` завжди має повертати `str`, ніколи `None` або виняток
- `memory_manager.update_async` daemon=True — смерть процесу не втрачає відповідь
- При будь-якій помилці profile: `interaction_count=0`, `expertise_level="intermediate"`, `preferred_style="conversational"`
---
## 11. Як не створити нескінченну рекурсію
**Три незалежні шари захисту:**
### Шар 1 — `activation_gate` (threading.local counter)
```
handle_message:
pre_check() → depth becomes 1
... робота ...
release() → depth back to 0
Якщо under_running_task викликає handle_message:
pre_check() → depth == 1 → MAX_DEPTH reached → return error response
```
`threading.local` — ізоляція per-thread, не заважає паралельним викликам з різних чатів.
### Шар 2 — `reflection_engine._REFLECTING` flag
- Глобальний (module-level) булевий прапорець
- Встановлюється в `True` перед LLM-рефлексією, скидається в `finally`
- Якщо рефлексія викличе щось що знову зайде в рефлексію → миттєво скидається
### Шар 3 — Архітектурна заборона
- Під-агенти (ops, iot, platform, spreadsheet, sustainability) мають `allow_delegation=False`
- Жоден агент не має знань про `handle_message` або `run.py`
- `depth_classifier`, `style_adapter`, `memory_manager` — pure functions, без CrewAI, без LLM
- Тільки `reflection_engine` (Deep mode) і фінальна задача Степана — LLM-виклики
---
## 12. Де саме інтегрувати
### 12.1 `crews/agromatrix_crew/run.py`
**Змінити:**
```python
# Новий imports (top)
from crews.agromatrix_crew.depth_classifier import classify, DepthDecision
from crews.agromatrix_crew.memory_manager import load_profiles, update_async
from crews.agromatrix_crew.style_adapter import build_prefix
from crews.agromatrix_crew.reflection_engine import reflect
from crews.agromatrix_crew.activation_gate import pre_check, release, select_crew
# handle_message:
# 1. pre_check (перше, до всього)
# 2. load_profiles (до classify)
# 3. classify (до побудови агентів)
# 4. if light → stepan_only_response
# 5. if deep → activation_gate.select_crew → run selected
# 6. reflect (після відповіді)
# 7. update_async (не блокуючий, daemon thread)
# 8. release (в finally)
```
**Зберегти:**
- Весь `route_operator_command` / `route_operator_text` (operator_commands не змінюємо)
- `tool_dictionary.normalize_from_text` + pending check (залишається до classify)
- `run_task_with_retry` (залишається для Deep mode)
- `audit_event` (залишається, розширюємо depth/mode в event)
- `farmos_ui_hint` (залишається)
**НЕ змінювати:**
- Сигнатуру `handle_message(text, user_id, chat_id, trace_id, ops_mode, last_pending_list)`
- Формат повернення (str, valid for JSON parse by http_api)
### 12.2 `crews/agromatrix_crew/operator_commands.py`
**Додати команди:**
```
/profile → показати UserProfile (user_id, expertise, style, last_seen, interaction_count)
/profile set <k>=<v> → оновити expertise_level або preferred_style
/farm → показати FarmProfile (коротко: поля, культури, інтеграції, сезон)
/farm update <k>=<v> → оновити seasonal_context.current_phase, порогові значення
```
**Зберегти без змін:**
- `/whoami`, `/pending`, `/approve`, `/reject`, `/apply_dict`, `/pending_stats`
- `is_operator()` — не змінювати
- `route_operator_command()` — розширити case, не переписувати
- `route_operator_text()` — залишити
**OPERATOR_COMMANDS set** — додати `"profile"`, `"farm"`.
### 12.3 `gateway-bot/http_api.py`
**Мінімальні зміни:**
- Додати env `AGX_FORCE_DEEP` → якщо "1", передавати в metadata або через handle_message (ops_mode вже є, можна додати depth_override parameter)
- **Нічого більше не змінювати.** handle_message вже приймає text, user_id, chat_id, trace_id, ops_mode.
**Не змінювати:**
- Маршрутизацію оператор/не-оператор (вже виправлена попереднім патчем)
- STEPAN_IMPORTS_OK logic
- doc_context logic
### 12.4 `memory-service`
**Не змінювати сервіс.** Використовуємо існуючий `/facts/upsert` і `/facts/get`.
**Нові fact-ключі:**
```
user_profile:agromatrix:{user_id} → UserProfile JSON (fact_value_json)
farm_profile:agromatrix:{user_id} → FarmProfile JSON (fact_value_json)
```
**memory_manager.py в crews** викликає memory-service по HTTP (sync httpx), URL з env:
```
AGX_MEMORY_SERVICE_URL=http://memory-service:8000
```
---
## 13. Схема файлів після впровадження
```
crews/agromatrix_crew/
├── __init__.py
├── run.py ← ЗМІНЕНО (нові модулі вмонтовані)
├── audit.py ← без змін
├── operator_commands.py ← РОЗШИРЕНО (/profile, /farm)
├── depth_classifier.py ← НОВИЙ
├── memory_manager.py ← НОВИЙ
├── style_adapter.py ← НОВИЙ
├── reflection_engine.py ← НОВИЙ
├── activation_gate.py ← НОВИЙ
├── agents/
│ ├── stepan_orchestrator.py ← backstory розширюється від style_adapter
│ ├── operations_agent.py ← без змін
│ ├── iot_agent.py ← без змін
│ ├── platform_agent.py ← без змін
│ ├── spreadsheet_agent.py ← без змін
│ └── sustainability_agent.py ← без змін
├── tasks/
│ ├── intake_and_plan.py ← без змін (лише для compatibility)
│ ├── execute_ops.py ← без змін
│ ├── execute_iot.py ← без змін
│ ├── execute_spreadsheets.py ← без змін
│ └── reporting.py ← без змін
└── tools/
└── __init__.py ← без змін
```
---
## 14. Порядок впровадження (поетапно)
**Фаза 1 — Foundation (без змін у run.py)**
1. `memory_manager.py` — реалізувати, написати unit-тест з mock memory-service
2. `depth_classifier.py` — реалізувати rule-based, написати тести по кожному патерну
3. `activation_gate.py` — реалізувати pre_check/release/select_crew, тест на рекурсію
**Фаза 2 — Light mode**
4. `style_adapter.py` — реалізувати три рівні і три стилі
5. Модифікувати `run.py`: вставити Light mode path (якщо light → пропустити всі crew)
6. Smoke-test: надіслати "привіт" → відповідь без crew
**Фаза 3 — Deep mode + Activation Gate**
7. Модифікувати `run.py`: Deep mode використовує `select_crew`, не всіх 5 агентів
8. Тест: `"сплануй тиждень"` → ops + sustainability, але не iot (якщо has_iot=false)
**Фаза 4 — Reflection + Profiles**
9. `reflection_engine.py` — rule-based Light reflection (без LLM)
10. Оновити `operator_commands.py``/profile`, `/farm`
11. E2E тест: 3 взаємодії → перевірка UserProfile накопичення
**Фаза 5 — Deep reflection (LLM)**
12. Додати LLM-рефлексію тільки для Deep mode
13. Тест на рекурсію: перевірити `_REFLECTING` flag спрацьовує
---
## 15. Метрики успіху
| Метрика | Ціль |
|---------|------|
| % запитів у Light mode (грітинги + прості) | > 30% від загального трафіку |
| Середній час відповіді Light mode | < 2s (без crew launch) |
| Середній час відповіді Deep mode | < 30s (тільки потрібні crew) |
| % запитів що запускають тільки 1-2 crew | > 50% від Deep запитів |
| Оператор `/profile` — відображає дані | 100% (якщо memory-service online) |
| Fallback без memory-service | Gateway не падає (fail-safe) |
| Рекурсивний виклик handle_message | 0 (activation_gate блокує) |
---
## 16. Відкриті питання (потрібно вирішити перед реалізацією)
1. **Sync vs async memory_manager**: `run.py` sync, але memory-service async-HTTP. Поточне рішення — sync httpx.Client. Альтернатива: asyncio.run() в окремому thread. Потребує рішення.
2. **UserProfile.expertise_level auto-upgrade**: поріг 10/30 взаємодій — достатньо? Або враховувати час між взаємодіями?
3. **reflection LLM model**: який LLM для рефлексії — той самий GPT-4, або дешевший GPT-3.5/Mistral? Вплив на latency та cost.
4. **FarmProfile cold-start**: перша взаємодія — profile порожній. Deep mode завжди? Або запитати у користувача дані ферми?
5. **Multi-user farm**: кілька операторів з однієї ферми — один FarmProfile чи кілька? Зараз `user_id`-based.
6. **Operator profile isolation**: оператор і звичайний користувач можуть мати одне user_id якщо оператор пише без оператор-чату. Чи потрібна окрема UserProfile для ops-mode?
---
*Документ готовий до review. Після погодження — розпочинати Фазу 1.*