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
162 lines
6.7 KiB
Python
162 lines
6.7 KiB
Python
"""
|
||
Depth Classifier для Степана.
|
||
|
||
classify_depth(text, has_doc_context, last_topic, user_profile) → "light" | "deep"
|
||
|
||
Без залежності від crewai — чистий Python.
|
||
Fail-closed: помилка → "deep".
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
from typing import Literal
|
||
|
||
from crews.agromatrix_crew.telemetry import tlog
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ─── Patterns ────────────────────────────────────────────────────────────────
|
||
|
||
_DEEP_ACTION_RE = re.compile(
|
||
r'\b(зроби|зробити|перевір|перевірити|порахуй|порахувати|підготуй|підготувати'
|
||
r'|онови|оновити|створи|створити|запиши|записати|зафіксуй|зафіксувати'
|
||
r'|внеси|внести|проаналізуй|проаналізувати|порівняй|порівняти'
|
||
r'|розрахуй|розрахувати|сплануй|спланувати|покажи|показати'
|
||
r'|заплануй|запланувати|закрий|закрити|відкрий|відкрити)\b',
|
||
re.IGNORECASE | re.UNICODE,
|
||
)
|
||
|
||
_DEEP_URGENT_RE = re.compile(
|
||
r'\b(аварія|терміново|критично|тривога|невідкладно|alert|alarm|critical)\b',
|
||
re.IGNORECASE | re.UNICODE,
|
||
)
|
||
|
||
_DEEP_DATA_RE = re.compile(
|
||
r'\b(\d[\d.,]*)\s*(га|кг|л|т|мм|°c|°f|%|гектар|літр|тонн)',
|
||
re.IGNORECASE | re.UNICODE,
|
||
)
|
||
|
||
_LIGHT_GREET_RE = re.compile(
|
||
r'^(привіт|добрий\s+\w+|доброго\s+\w+|hello|hi|hey|ок|окей|добре|зрозумів|зрозуміла'
|
||
r'|дякую|дякуй|спасибі|чудово|супер|ясно|зрозуміло|вітаю|вітання)[\W]*$',
|
||
re.IGNORECASE | re.UNICODE,
|
||
)
|
||
|
||
_DEEP_INTENTS = frozenset({
|
||
'plan_week', 'plan_day', 'plan_vs_fact', 'show_critical_tomorrow', 'close_plan'
|
||
})
|
||
|
||
# ─── Intent detection (inline, no crewai dependency) ─────────────────────────
|
||
|
||
def _detect_intent(text: str) -> str:
|
||
t = text.lower()
|
||
if 'сплануй' in t and 'тиж' in t:
|
||
return 'plan_week'
|
||
if 'сплануй' in t:
|
||
return 'plan_day'
|
||
if 'критично' in t or 'на завтра' in t:
|
||
return 'show_critical_tomorrow'
|
||
if 'план/факт' in t or 'план факт' in t:
|
||
return 'plan_vs_fact'
|
||
if 'закрий план' in t:
|
||
return 'close_plan'
|
||
return 'general'
|
||
|
||
|
||
# ─── Public API ───────────────────────────────────────────────────────────────
|
||
|
||
def classify_depth(
|
||
text: str,
|
||
has_doc_context: bool = False,
|
||
last_topic: str | None = None,
|
||
user_profile: dict | None = None,
|
||
session: dict | None = None,
|
||
) -> Literal["light", "deep"]:
|
||
"""
|
||
Визначає глибину обробки запиту.
|
||
|
||
light — Степан відповідає сам, без запуску під-агентів
|
||
deep — повний orchestration flow з делегуванням
|
||
|
||
v3: session — SessionContext; якщо last_depth=="light" і короткий follow-up
|
||
без action verbs → stability_guard повертає "light" без подальших перевірок.
|
||
|
||
Правило fail-closed: при будь-якій помилці повертає "deep".
|
||
"""
|
||
try:
|
||
t = text.strip()
|
||
|
||
# ── Intent Stability Guard (v3) ────────────────────────────────────────
|
||
# Якщо попередня взаємодія була light і поточне повідомлення ≤6 слів
|
||
# без action verbs / urgent → утримуємо в light без зайвих перевірок.
|
||
if (
|
||
session
|
||
and session.get("last_depth") == "light"
|
||
and not _DEEP_ACTION_RE.search(t)
|
||
and not _DEEP_URGENT_RE.search(t)
|
||
):
|
||
word_count_guard = len(t.split())
|
||
if word_count_guard <= 6:
|
||
tlog(logger, "stability_guard_triggered", chat_id="n/a",
|
||
words=word_count_guard, last_depth="light")
|
||
return "light"
|
||
|
||
# Explicit greetings / social acks → always light
|
||
if _LIGHT_GREET_RE.match(t):
|
||
tlog(logger, "depth", depth="light", reason="greeting")
|
||
return "light"
|
||
|
||
word_count = len(t.split())
|
||
|
||
# Follow-up heuristic: ≤6 words + last_topic + no action verbs + no urgent → light
|
||
# Handles: "а на завтра?", "а по полю 12?", "а якщо дощ?" etc.
|
||
if (
|
||
word_count <= 6
|
||
and last_topic is not None
|
||
and not _DEEP_ACTION_RE.search(t)
|
||
and not _DEEP_URGENT_RE.search(t)
|
||
):
|
||
tlog(logger, "depth", depth="light", reason="short_followup_last_topic",
|
||
words=word_count, last_topic=last_topic)
|
||
return "light"
|
||
|
||
# Very short follow-ups without last_topic → light (≤4 words, no verbs)
|
||
if word_count <= 4 and not _DEEP_ACTION_RE.search(t) and not _DEEP_URGENT_RE.search(t):
|
||
tlog(logger, "depth", depth="light", reason="short_followup", words=word_count)
|
||
return "light"
|
||
|
||
# Active doc context → deep
|
||
if has_doc_context:
|
||
tlog(logger, "depth", depth="deep", reason="has_doc_context")
|
||
return "deep"
|
||
|
||
# Urgency keywords → always deep
|
||
if _DEEP_URGENT_RE.search(t):
|
||
tlog(logger, "depth", depth="deep", reason="urgent_keyword")
|
||
return "deep"
|
||
|
||
# Explicit action verbs → deep
|
||
if _DEEP_ACTION_RE.search(t):
|
||
tlog(logger, "depth", depth="deep", reason="action_verb")
|
||
return "deep"
|
||
|
||
# Numeric measurements → deep
|
||
if _DEEP_DATA_RE.search(t):
|
||
tlog(logger, "depth", depth="deep", reason="numeric_data")
|
||
return "deep"
|
||
|
||
# Intent-based deep trigger
|
||
detected = _detect_intent(t)
|
||
if detected in _DEEP_INTENTS:
|
||
tlog(logger, "depth", depth="deep", reason="intent", intent=detected)
|
||
return "deep"
|
||
|
||
tlog(logger, "depth", depth="light", reason="no_deep_signal")
|
||
return "light"
|
||
|
||
except Exception as exc:
|
||
logger.warning("classify_depth error, defaulting to deep: %s", exc)
|
||
return "deep"
|