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

165 lines
7.0 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.
"""
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