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
This commit is contained in:
Apple
2026-03-03 07:14:14 -08:00
parent e9dedffa48
commit 129e4ea1fc
241 changed files with 69349 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
"""
doc_focus.py — Doc Focus Gate helpers (v3.5 / v3.6 / v3.7).
Без залежностей від crewai/agromatrix_tools — тільки re і stdlib.
Імпортується з run.py і operator_commands.py.
Публічні функції:
_is_doc_question(text) → bool
_detect_domain(text, logger) → str
detect_context_signals(text) → dict
build_mode_clarifier(text) → str
handle_doc_focus(sub, chat_id) → dict
"""
from __future__ import annotations
import re
import time
# ── Тригери: повідомлення явно про документ ──────────────────────────────────
_DOC_QUESTION_RE = re.compile(
r"звіт|документ|таблиц|xlsx|sheet|рядок|колонк|в\s+звіті|у\s+файлі|у\s+документі"
r"|по\s+звіту|з\s+(?:цього\s+)?файлу|в\s+цьому\s+документі|по\s+документу"
r"\s+документа|відкрий\s+звіт",
re.IGNORECASE | re.UNICODE,
)
# Фінансові тригери ТІЛЬКИ якщо є прив'язка до "документу/файлу"
_DOC_FINANCIAL_RE = re.compile(
r"(?:прибуток|витрати?|собівартість|дохід|надходж|виручк|добрив|насінн|площ|гектар|грн|грн/га)"
r".*(?:звіт|документ|файл|xlsx)|"
r"(?:звіт|документ|файл|xlsx).*(?:прибуток|витрати?|дохід|грн|грн/га|площ)",
re.IGNORECASE | re.UNICODE,
)
# ── Explicit doc-токени (перемагають vision) ─────────────────────────────────
_EXPLICIT_DOC_TOKEN_RE = re.compile(
r"по\s+звіту|у\s+файлі|в\s+файлі|у\s+документі|в\s+документі|з\s+таблиц"
r"|у\s+звіті|в\s+звіті|по\s+документу|з\s+документ|у\s+цьому\s+(?:файлі|звіті|документі)",
re.IGNORECASE | re.UNICODE,
)
# ── Тригери що СКАСОВУЮТЬ doc-режим ──────────────────────────────────────────
_URL_RE = re.compile(r"https?://\S+", re.IGNORECASE)
_VISION_RE = re.compile(
r"фото|картинк|зображенн|листя|плями|шкідник|хвороба|бур'ян|бурян"
r"|рослин|гриб|гниль|хлороз|некроз|личинк|жук|кліщ|тля",
re.IGNORECASE | re.UNICODE,
)
_ACTION_OPS_RE = re.compile(
r"^(?:зроби|план|внеси|зафіксуй|перевір|порахуй|додай|видали|оновни|відкрий|нагадай)",
re.IGNORECASE | re.UNICODE,
)
_WEB_INTENT_RE = re.compile(
r"каталог|сайт|посиланн|переглянь\s+сторінк|вивч[иі]\s+каталог|знайди\s+на\s+сайт",
re.IGNORECASE | re.UNICODE,
)
# ── v3.6: Fact-signal — числові запити без прив'язки до "звіту" ──────────────
_FACT_UNITS_RE = re.compile(
r"грн|uah|₴|га\b|ha\b|%|грн/га|uah/ha|тис\.?|млн\.?|\d+\s*(?:грн|га|ha|%)",
re.IGNORECASE | re.UNICODE,
)
_FACT_WORDS_RE = re.compile(
r"прибуток|витрати?|виручка|дохід|маржа|площа|добрива|насіння|паливо|оренда|собівартість",
re.IGNORECASE | re.UNICODE,
)
# ── v3.7: UX-фрази для заміни ────────────────────────────────────────────────
_DOC_AWARENESS_RE = re.compile(
r"(так,\s*пам['\u2019]ятаю|не\s+бачу\s+його|не\s+бачу\s+перед\s+собою"
r"|мені\s+(?:не\s+)?доступний\s+документ)",
re.IGNORECASE | re.UNICODE,
)
_VISION_INTRO_RE = re.compile(
r"^на\s+фото\s+видно",
re.IGNORECASE | re.UNICODE,
)
def _is_doc_question(text: str) -> bool:
"""
Rule-based: чи питання явно про документ/звіт.
Explicit doc-токен перемагає vision-слова (скрін таблиці + caption).
Fail-safe: будь-яка помилка → False.
"""
try:
t = text.strip()
if _URL_RE.search(t):
return False
if _WEB_INTENT_RE.search(t):
return False
if _EXPLICIT_DOC_TOKEN_RE.search(t):
return True
if _VISION_RE.search(t):
return False
if _DOC_QUESTION_RE.search(t):
return True
if _DOC_FINANCIAL_RE.search(t):
return True
return False
except Exception:
return False
def _detect_domain(text: str, logger=None) -> str:
"""
Визначає домен повідомлення.
Повертає: "doc" | "vision" | "web" | "ops" | "general"
Пріоритети:
URL/web > explicit_doc_token > загальні doc-тригери > vision > ops > general
Порожній текст (caption відсутній) → "vision".
"""
try:
t = text.strip()
if not t:
return "vision"
if _URL_RE.search(t) or _WEB_INTENT_RE.search(t):
return "web"
if _EXPLICIT_DOC_TOKEN_RE.search(t):
if _VISION_RE.search(t) and logger:
try:
logger.info(
"AGX_STEPAN_METRIC domain_override from=vision to=doc reason=explicit_doc_tokens"
)
except Exception:
pass
return "doc"
if _DOC_QUESTION_RE.search(t) or _DOC_FINANCIAL_RE.search(t):
return "doc"
if _VISION_RE.search(t):
return "vision"
if _ACTION_OPS_RE.search(t):
return "ops"
return "general"
except Exception:
return "general"
def detect_context_signals(text: str) -> dict:
"""
v3.6: Повертає словник булевих сигналів для doc-mode gating.
Ключі:
has_explicit_doc_token: bool — "по звіту", "у файлі" тощо
has_doc_trigger: bool — загальні doc-тригери (звіт, документ)
has_vision_trigger: bool — листя, шкідник, фото...
has_url: bool — http(s)://...
has_web_intent: bool — каталог, сайт...
has_fact_signal: bool — числові одиниці або фін-слова
"""
try:
t = text.strip()
return {
"has_explicit_doc_token": bool(_EXPLICIT_DOC_TOKEN_RE.search(t)),
"has_doc_trigger": bool(
_DOC_QUESTION_RE.search(t) or _DOC_FINANCIAL_RE.search(t)
),
"has_vision_trigger": bool(_VISION_RE.search(t)),
"has_url": bool(_URL_RE.search(t)),
"has_web_intent": bool(_WEB_INTENT_RE.search(t)),
"has_fact_signal": bool(_FACT_UNITS_RE.search(t) or _FACT_WORDS_RE.search(t)),
}
except Exception:
return {
"has_explicit_doc_token": False, "has_doc_trigger": False,
"has_vision_trigger": False, "has_url": False,
"has_web_intent": False, "has_fact_signal": False,
}
def build_mode_clarifier(text: str) -> str:
"""
v3.6/v3.7: Одне контекстне уточнююче питання (без "!", без "будь ласка").
URL → "Ти про посилання чи про звіт?"
vision → "Це про фото чи про цифри зі звіту?"
facts → "Це про конкретні цифри зі звіту?"
інше → "Йдеться про звіт чи про інше?"
"""
try:
t = text.strip()
if _URL_RE.search(t):
return "Ти про посилання чи про звіт?"
if _VISION_RE.search(t):
return "Це про фото чи про цифри зі звіту?"
if _FACT_UNITS_RE.search(t) or _FACT_WORDS_RE.search(t):
return "Це про конкретні цифри зі звіту?"
return "Йдеться про звіт чи про інше?"
except Exception:
return "Йдеться про звіт чи про інше?"
def handle_doc_focus(sub: str, chat_id: str | None = None) -> dict:
"""
/doc [on|off|status].
/doc on → doc_focus=True, TTL = DOC_FOCUS_TTL, cooldown скинутий
/doc off → doc_focus=False
/doc status → поточний стан (focus, ttl_left, cooldown_left, active_doc_id, facts)
"""
def _wrap(msg: str) -> dict:
return {"ok": True, "message": msg}
try:
from crews.agromatrix_crew.session_context import (
_STORE, DOC_FOCUS_TTL, is_doc_focus_active, load_session,
is_doc_focus_cooldown_active,
)
except ImportError:
return _wrap("session_context not available")
if not chat_id:
return _wrap("chat_id required for /doc command")
now = time.time()
if sub == "on":
existing = _STORE.get(str(chat_id)) or {}
existing["doc_focus"] = True
existing["doc_focus_ts"] = now
existing["doc_focus_cooldown_until"] = 0.0 # /doc on скидає cooldown
_STORE[str(chat_id)] = existing
doc_id = existing.get("active_doc_id") or ""
return _wrap(f"doc_focus=on. Документ: {str(doc_id)[:20]}. TTL={int(DOC_FOCUS_TTL)}с.")
if sub == "off":
existing = _STORE.get(str(chat_id)) or {}
existing["doc_focus"] = False
existing["doc_focus_ts"] = 0.0
_STORE[str(chat_id)] = existing
return _wrap("doc_focus=off. Степан відповідатиме без прив'язки до документа.")
# status (default)
session = load_session(str(chat_id))
focus_active = is_doc_focus_active(session, now)
cooldown_active = is_doc_focus_cooldown_active(session, now)
doc_id = session.get("active_doc_id") or ""
doc_facts = session.get("doc_facts") or {}
ttl_left = max(0.0, DOC_FOCUS_TTL - (now - (session.get("doc_focus_ts") or 0.0)))
cooldown_left = max(0.0, (session.get("doc_focus_cooldown_until") or 0.0) - now)
facts_keys = (
", ".join(k for k in doc_facts if k not in ("conflicts", "needs_recheck"))
if doc_facts else ""
)
cooldown_str = f" cooldown={int(cooldown_left)}с" if cooldown_active else ""
return _wrap(
f"doc_focus={'on' if focus_active else 'off'} "
f"ttl_left={int(ttl_left)}с{cooldown_str} | "
f"active_doc_id={str(doc_id)[:20]} | "
f"facts=[{facts_keys}]"
)