Files
microdao-daarion/crews/agromatrix_crew/farm_state.py
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store,
architecture_pressure, backlog_generator/store, cost_analyzer, data_governance,
dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment,
platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files),
signature_state_store, sofiia_auto_router, tool_governance

New services:
- sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static
- memory-service: integration_endpoints, integrations, voice_endpoints, static UI
- aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents)
- sofiia-supervisor: new supervisor service
- aistalk-bridge-lite: Telegram bridge lite
- calendar-service: CalDAV calendar service with reminders
- mlx-stt-service / mlx-tts-service: Apple Silicon speech services
- binance-bot-monitor: market monitor service
- node-worker: STT/TTS memory providers

New tools (9): agent_email, browser_tool, contract_tool, observability_tool,
oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault

New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus,
farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine,
session_context, style_adapter, telemetry)

Tests: 85+ test files for all new modules
Made-with: Cursor
2026-03-03 07:14:14 -08:00

209 lines
8.7 KiB
Python
Raw Permalink 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.
"""
farm_state.py — v4 Farm State Layer.
Сесійний оперативний контекст господарства.
Ізольований від doc_mode, memory_manager, crewai.
Публічні функції:
detect_farm_state_updates(text) -> dict
update_farm_state(session, updates, now_ts) -> None
build_farm_state_prefix(session) -> str
"""
from __future__ import annotations
import re
import time
# ── Культури ──────────────────────────────────────────────────────────────────
_CROP_RE = re.compile(
r"\b(кукурудз[аиіує]|кукурудзою|кукурудзі"
r"|пшениц[яіює]|пшениця"
r"|соняшник[аиуів]?|соняшник"
r"|ріпак[аиуів]?|ріпак"
r"|со[яіює]|соя"
r"|ячмінь|ячмен[юі]"
r"|горох[аиуів]?|горох"
r"|буряк[аиуів]?|буряк"
r"|картопл[яіі]|картопля"
r"|льон[аиуів]?|льон)\b",
re.IGNORECASE | re.UNICODE,
)
# Нормалізація до канонічної форми
_CROP_CANONICAL: dict[str, str] = {
# кукурудза (всі відмінки)
"кукурудза": "кукурудза", "кукурудзи": "кукурудза",
"кукурудзі": "кукурудза", "кукурудзу": "кукурудза",
"кукурудзою": "кукурудза", "кукурудзє": "кукурудза",
# пшениця
"пшениця": "пшениця", "пшениці": "пшениця",
"пшеницею": "пшениця", "пшеницю": "пшениця", "пшеницю": "пшениця",
# соняшник
"соняшник": "соняшник", "соняшника": "соняшник",
"соняшнику": "соняшник", "соняшників": "соняшник",
# ріпак
"ріпак": "ріпак", "ріпака": "ріпак", "ріпаку": "ріпак", "ріпаків": "ріпак",
# соя
"соя": "соя", "сої": "соя", "сою": "соя", "соєю": "соя",
# ячмінь
"ячмінь": "ячмінь", "ячменю": "ячмінь", "ячмені": "ячмінь",
# горох
"горох": "горох", "гороху": "горох", "гороха": "горох", "горохів": "горох",
# буряк
"буряк": "буряк", "буряка": "буряк", "буряку": "буряк", "буряків": "буряк",
# картопля
"картопля": "картопля", "картоплі": "картопля",
# льон
"льон": "льон", "льону": "льон", "льона": "льон", "льонів": "льон",
}
# ── Стадії росту ──────────────────────────────────────────────────────────────
# Спочатку шукаємо числові коди (vN, rN, BBCH) — вони точніші.
# Потім словесні фази. "стадія" — артикль, ігноруємо.
_STAGE_NUMERIC_RE = re.compile(
r"\b(v\d{1,2}|vt|r\d|bbch\s*\d+|\d+-\d+\s+листк[иів]?)\b",
re.IGNORECASE | re.UNICODE,
)
_STAGE_WORD_RE = re.compile(
r"\b(сходи|кущення|викидання\s+волоті|цвітіння|наливання\s+зерна"
r"|дозрівання|збирання|посів|кінець\s+вегетації)\b",
re.IGNORECASE | re.UNICODE,
)
# Єдиний RE для API-сумісності (використовуємо numeric першим)
_STAGE_RE = _STAGE_NUMERIC_RE # backward compat alias
# ── Проблеми / симптоми ───────────────────────────────────────────────────────
_ISSUE_RE = re.compile(
r"\b(жовтизна|жовтіння|хлороз|некроз|плям[иа]|плями"
r"|дефіцит\s+\w+|нестача\s+\w+"
r"|шкідник[иів]?|хвороб[аи]|гриб[иок]|гниль"
r"|бур['']?ян[иів]?|бур['']яни"
r"|попелиц[яі]|тля|кліщ[іи]|трипс[иів]?"
r"|фузаріоз|іржа|борошниста\s+роса|септоріоз)\b",
re.IGNORECASE | re.UNICODE,
)
# ── Ризики ────────────────────────────────────────────────────────────────────
_RISK_RE = re.compile(
r"\b(посуха|посухи|засух[аи]"
r"|заморозок|заморозки|приморозок"
r"|спека|перегрів"
r"|надлишок\s+вологи|затоплення|підтоплення"
r"|град|вітер|буря"
r"|брак\s+опадів|немає\s+дощу)\b",
re.IGNORECASE | re.UNICODE,
)
# Максимальний TTL farm_state в сесії (30 хв — синхронізовано з SESSION_TTL)
FARM_STATE_TTL = 1800.0
def detect_farm_state_updates(text: str) -> dict:
"""
Rule-based витяг оновлень farm_state з тексту.
Повертає тільки знайдені поля:
current_crop: str
growth_stage: str
recent_issue: str
risk_flags: list[str]
Fail-safe: будь-яка помилка → {}.
"""
try:
t = text.strip()
updates: dict = {}
crop_m = _CROP_RE.search(t)
if crop_m:
raw = crop_m.group(0).lower()
updates["current_crop"] = _CROP_CANONICAL.get(raw, raw)
# Числовий код (V6, R2, BBCH30) пріоритетніший за словесну фазу
stage_m = _STAGE_NUMERIC_RE.search(t) or _STAGE_WORD_RE.search(t)
if stage_m:
updates["growth_stage"] = stage_m.group(0).strip().upper()
issue_m = _ISSUE_RE.search(t)
if issue_m:
updates["recent_issue"] = issue_m.group(0).strip().lower()
risk_matches = _RISK_RE.findall(t)
if risk_matches:
updates["risk_flags"] = [r.lower() for r in risk_matches]
return updates
except Exception:
return {}
def update_farm_state(session: dict, updates: dict, now_ts: float | None = None) -> None:
"""
Оновлює session["farm_state"] знайденими полями.
Створює dict якщо відсутній.
Встановлює last_update_ts.
Fail-safe: не кидає назовні.
"""
try:
if not updates:
return
now = now_ts if now_ts is not None else time.time()
fs: dict = session.get("farm_state") or {}
if "current_crop" in updates:
fs["current_crop"] = updates["current_crop"]
if "growth_stage" in updates:
fs["growth_stage"] = updates["growth_stage"]
if "recent_issue" in updates:
fs["recent_issue"] = updates["recent_issue"]
if "risk_flags" in updates:
existing_risks: list = fs.get("risk_flags") or []
new_risks = updates["risk_flags"]
# merge + dedup, max 5
merged = list(dict.fromkeys(existing_risks + new_risks))[:5]
fs["risk_flags"] = merged
fs["last_update_ts"] = now
session["farm_state"] = fs
except Exception:
pass
def build_farm_state_prefix(session: dict, now_ts: float | None = None) -> str:
"""
Повертає короткий структурований префікс якщо є farm_state.
Максимум 5 рядків.
Порожній рядок якщо нема current_crop або state протух.
Fail-safe: будь-яка помилка → "".
"""
try:
fs: dict = session.get("farm_state") or {}
if not fs.get("current_crop"):
return ""
# TTL check
last_ts = float(fs.get("last_update_ts") or 0.0)
now = now_ts if now_ts is not None else time.time()
if (now - last_ts) > FARM_STATE_TTL:
return ""
lines = ["[Контекст господарства]"]
lines.append(f"Культура: {fs['current_crop']}")
if fs.get("growth_stage"):
lines.append(f"Стадія: {fs['growth_stage']}")
if fs.get("recent_issue"):
lines.append(f"Проблема: {fs['recent_issue']}")
risks = fs.get("risk_flags") or []
if risks:
lines.append(f"Ризики: {', '.join(risks[:3])}")
return "\n".join(lines)
except Exception:
return ""