Files
microdao-daarion/crews/agromatrix_crew/depth_classifier.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

162 lines
6.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.
"""
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"