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
691 lines
32 KiB
Markdown
691 lines
32 KiB
Markdown
# 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.*
|