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
165 lines
7.0 KiB
Python
165 lines
7.0 KiB
Python
"""
|
||
Soft Proactivity Layer — Humanized Stepan v3.
|
||
|
||
Додає РІВНО 1 коротке речення в кінець deep-відповіді за суворих умов.
|
||
Rule-based, без LLM.
|
||
|
||
Умови спрацювання (всі мають виконуватись одночасно):
|
||
1. depth == "deep"
|
||
2. reflection is None OR reflection["confidence"] >= 0.7
|
||
3. interaction_count % 10 == 0 (кожна 10-та взаємодія)
|
||
4. В known_intents один intent зустрівся >= 3 рази
|
||
5. НЕ (preferred_style == "brief" AND response вже містить "?")
|
||
|
||
Речення ≤ 120 символів, без "!".
|
||
|
||
Telemetry:
|
||
AGX_STEPAN_METRIC proactivity_added user_id=h:... intent=... style=...
|
||
AGX_STEPAN_METRIC proactivity_skipped reason=... (якщо умови не пройдені)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import random
|
||
from typing import Any
|
||
|
||
from crews.agromatrix_crew.telemetry import tlog
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ─── Phrase banks ─────────────────────────────────────────────────────────────
|
||
|
||
_PROACTIVE_GENERIC = [
|
||
"За потреби можу швидко зібрати план/факт за вчора.",
|
||
"Якщо хочеш, можу підготувати короткий чек-лист на ранок.",
|
||
"Можу також порівняти з попереднім тижнем — скажи якщо потрібно.",
|
||
"Якщо зміниться пріоритет — одразу скажи, скорегуємо.",
|
||
"Якщо потрібна деталізація по конкретному полю — кажи.",
|
||
"Готовий зібрати зведення по полях якщо буде потреба.",
|
||
"Можу також перевірити статуси по відкритих задачах.",
|
||
]
|
||
|
||
_PROACTIVE_IOT = [
|
||
"Якщо хочеш, перевірю датчики по ключових полях.",
|
||
"Можу також відслідкувати вологість по полях у реальному часі.",
|
||
"За потреби — швидкий звіт по датчиках.",
|
||
"Якщо є аномалії на датчиках — дам знати одразу.",
|
||
]
|
||
|
||
_PROACTIVE_PLAN = [
|
||
"За потреби можу оновити план після нових даних.",
|
||
"Якщо хочеш — зведу всі задачі на тиждень в один список.",
|
||
"Можу ще раз пройтись по пріоритетах якщо щось зміниться.",
|
||
"Якщо план зміниться — оновлю фільтри автоматично.",
|
||
]
|
||
|
||
_PROACTIVE_SUSTAINABILITY = [
|
||
"Можу також подивитись показники сталості за вибраний період.",
|
||
"Якщо потрібно — порівняємо з нормою по регіону.",
|
||
]
|
||
|
||
# intent → bank mapping
|
||
_INTENT_BANK: dict[str, list[str]] = {
|
||
"iot_sensors": _PROACTIVE_IOT,
|
||
"plan_day": _PROACTIVE_PLAN,
|
||
"plan_week": _PROACTIVE_PLAN,
|
||
"plan_vs_fact": _PROACTIVE_PLAN,
|
||
"sustainability": _PROACTIVE_SUSTAINABILITY,
|
||
}
|
||
|
||
|
||
def _top_intent(known_intents: list | None) -> tuple[str | None, int]:
|
||
"""
|
||
Знаходить intent з найвищою частотою у known_intents.
|
||
known_intents = list[str] (повторення дозволені, кожен запис = 1 взаємодія).
|
||
Повертає (intent, count) або (None, 0).
|
||
"""
|
||
if not known_intents:
|
||
return None, 0
|
||
freq: dict[str, int] = {}
|
||
for item in known_intents:
|
||
if isinstance(item, str):
|
||
freq[item] = freq.get(item, 0) + 1
|
||
if not freq:
|
||
return None, 0
|
||
top = max(freq, key=lambda k: freq[k])
|
||
return top, freq[top]
|
||
|
||
|
||
def maybe_add_proactivity(
|
||
response: str,
|
||
user_profile: dict,
|
||
depth: str,
|
||
reflection: dict | None = None,
|
||
) -> tuple[str, bool]:
|
||
"""
|
||
Можливо додає 1 проактивне речення до відповіді.
|
||
|
||
Аргументи:
|
||
response — поточна відповідь Степана
|
||
user_profile — UserProfile dict
|
||
depth — "light" або "deep"
|
||
reflection — результат reflect_on_response або None
|
||
|
||
Повертає:
|
||
(new_response, was_added: bool)
|
||
"""
|
||
user_id = user_profile.get("user_id", "")
|
||
|
||
try:
|
||
# Умова 1: тільки deep
|
||
if depth != "deep":
|
||
tlog(logger, "proactivity_skipped", user_id=user_id, reason="not_deep")
|
||
return response, False
|
||
|
||
# Умова 2: confidence >= 0.7 або reflection відсутній
|
||
if reflection is not None:
|
||
confidence = reflection.get("confidence", 1.0)
|
||
if confidence < 0.7:
|
||
tlog(logger, "proactivity_skipped", user_id=user_id,
|
||
reason="low_confidence", confidence=round(confidence, 2))
|
||
return response, False
|
||
|
||
# Умова 3: interaction_count % 10 == 0
|
||
count = user_profile.get("interaction_count", 0)
|
||
if count == 0 or count % 10 != 0:
|
||
tlog(logger, "proactivity_skipped", user_id=user_id,
|
||
reason="not_tenth", interaction_count=count)
|
||
return response, False
|
||
|
||
# Умова 4: top intent зустрічався >= 3 рази
|
||
known_intents = user_profile.get("known_intents", [])
|
||
top_intent, top_count = _top_intent(known_intents)
|
||
if top_count < 3:
|
||
tlog(logger, "proactivity_skipped", user_id=user_id,
|
||
reason="intent_freq_low", top_intent=top_intent, top_count=top_count)
|
||
return response, False
|
||
|
||
# Умова 5: не нав'язувати якщо brief і вже є питання
|
||
preferred_style = user_profile.get("preferences", {}).get("report_format", "")
|
||
style = user_profile.get("style", "")
|
||
is_brief = preferred_style == "brief" or style == "concise"
|
||
if is_brief and "?" in response:
|
||
tlog(logger, "proactivity_skipped", user_id=user_id,
|
||
reason="brief_with_question", style=style)
|
||
return response, False
|
||
|
||
# Обрати банк фраз за intent
|
||
bank = _INTENT_BANK.get(top_intent or "", _PROACTIVE_GENERIC)
|
||
seed = hash(f"{user_id}:{count}") % (2**32)
|
||
rng = random.Random(seed)
|
||
phrase = rng.choice(bank)
|
||
|
||
# Гарантуємо ≤ 120 символів і без "!"
|
||
phrase = phrase[:120].replace("!", "")
|
||
|
||
new_response = response.rstrip() + "\n\n" + phrase
|
||
tlog(logger, "proactivity_added", user_id=user_id,
|
||
intent=top_intent, style=style)
|
||
return new_response, True
|
||
|
||
except Exception as exc:
|
||
logger.warning("maybe_add_proactivity error (no-op): %s", exc)
|
||
return response, False
|