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

227 lines
9.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.
"""
Reflection Engine для Степана (Deep mode only).
reflect_on_response(user_input, final_response, user_profile, farm_profile)
→ dict з полями: new_facts, style_shift, confidence, clarifying_question
Правила:
- НЕ генерує нову відповідь, тільки аналізує
- НЕ запускається в Light mode
- НЕ запускається рекурсивно (_REFLECTING flag)
- При будь-якій помилці → повертає safe_fallback()
- confidence < 0.6 → викликаючий код може додати clarifying_question до відповіді
Anti-recursion:
Три рівні захисту:
1. Модульний boolean _REFLECTING (per-process, cleared у finally)
2. Caller у run.py передає depth="deep" — reflection ніколи не викличе handle_message
3. Reflection не імпортує run.py, не використовує Crew/Agent
Fail-safe: повертає safe_fallback() при будь-якому винятку.
"""
from __future__ import annotations
import logging
import re
from typing import Any
logger = logging.getLogger(__name__)
# ─── Anti-recursion guard ─────────────────────────────────────────────────────
_REFLECTING: bool = False
def _safe_fallback() -> dict[str, Any]:
return {
"new_facts": {},
"style_shift": None,
"confidence": 1.0,
"clarifying_question": None,
}
# ─── Fact extraction (rule-based) ────────────────────────────────────────────
_CROP_RE = re.compile(
r'\b(пшениця|кукурудза|соняшник|ріпак|соя|ячмінь|жито|гречка|овес|цукровий\s+буряк)\b',
re.IGNORECASE | re.UNICODE,
)
_REGION_RE = re.compile(
r'\b(область|район|село|місто|регіон|зона)\s+([\w-]+)',
re.IGNORECASE | re.UNICODE,
)
_ROLE_RE = re.compile(
r'\b(я\s+)?(агроном|власник|господар|оператор|механік|агрономка|директор)\b',
re.IGNORECASE | re.UNICODE,
)
_NAME_RE = re.compile(
r'\b(мене\s+звуть|я\s+[-—]?\s*|мене\s+кличуть)\s+([АІЇЄA-Z][аіїєa-z]{2,})',
re.UNICODE,
)
_STYLE_SIGNAL: dict[str, list[str]] = {
"concise": ["коротко", "стисло", "без деталей"],
"checklist": ["списком", "маркерами", "пунктами"],
"analytical": ["аналіз", "причини", "наслідки"],
"detailed": ["детально", "докладно", "розгорнуто"],
}
_UNCERTAINTY_PHRASES = [
"не впевнений", "не зрозуміло", "не знаю", "можливо", "мабуть",
"не ясно", "незрозуміло", "не зрозумів", "не визначив", "відсутні дані",
"потрібно уточнити", "уточніть",
]
def _extract_new_facts(user_input: str, response: str, user_profile: dict | None, farm_profile: dict | None) -> dict:
facts: dict[str, Any] = {}
up = user_profile or {}
fp = farm_profile or {}
# Name
m = _NAME_RE.search(user_input)
if m and not up.get("name"):
facts["name"] = m.group(2)
# Role
m = _ROLE_RE.search(user_input)
if m and up.get("role") == "unknown":
role_map = {
"агроном": "agronomist", "агрономка": "agronomist",
"власник": "owner", "господар": "owner", "директор": "owner",
"оператор": "operator", "механік": "mechanic",
}
raw = m.group(2).lower()
for k, v in role_map.items():
if k in raw:
facts["role"] = v
break
# Crops (new ones not yet in farm profile)
existing_crops = set(fp.get("crops", []))
found_crops = {m.group(0).lower() for m in _CROP_RE.finditer(user_input)}
new_crops = found_crops - existing_crops
if new_crops:
facts["new_crops"] = list(new_crops)
# Style shift from user phrasing
tl = user_input.lower()
for style, signals in _STYLE_SIGNAL.items():
if any(s in tl for s in signals) and up.get("style") != style:
facts["style_shift"] = style
break
return facts
def _compute_confidence(user_input: str, response: str) -> float:
"""
Оцінити впевненість відповіді (0..1).
Низька впевненість якщо відповідь містить ознаки невизначеності.
"""
resp_lower = response.lower()
uncertainty_count = sum(1 for ph in _UNCERTAINTY_PHRASES if ph in resp_lower)
if uncertainty_count >= 3:
return 0.4
if uncertainty_count >= 1:
return 0.55
# Response too short for the complexity of the question
if len(response) < 80 and len(user_input) > 150:
return 0.5
return 0.85
def _build_clarifying_question(user_input: str, response: str, facts: dict) -> str | None:
"""
Сформувати одне уточнювальне питання якщо потрібно.
Повертає None якщо питання не потрібне.
"""
if facts.get("new_crops"):
crops_str = ", ".join(facts["new_crops"])
return f"Уточніть: ці культури ({crops_str}) відносяться до поточного сезону?"
resp_lower = response.lower()
if "потрібно уточнити" in resp_lower or "уточніть" in resp_lower:
# Response itself already asks; no need to double
return None
if "не зрозуміло" in resp_lower or "не визначив" in resp_lower:
return "Чи можете уточнити — що саме вас цікавить найбільше?"
return None
# ─── Public API ───────────────────────────────────────────────────────────────
def reflect_on_response(
user_input: str,
final_response: str,
user_profile: dict | None,
farm_profile: dict | None,
) -> dict[str, Any]:
"""
Аналізує відповідь після Deep mode.
Повертає:
{
"new_facts": dict — нові факти для запису в профіль
"style_shift": str | None — новий стиль якщо виявлено
"confidence": float 0..1 — впевненість відповіді
"clarifying_question": str | None — питання для користувача якщо confidence < 0.6
}
НЕ запускається рекурсивно.
Fail-safe: будь-який виняток → _safe_fallback().
"""
global _REFLECTING
if _REFLECTING:
from crews.agromatrix_crew.telemetry import tlog as _tlog
_tlog(logger, "reflection_skip", reason="recursion_guard")
logger.warning("reflection_engine: recursion guard active — skipping")
return _safe_fallback()
_REFLECTING = True
try:
if not user_input or not final_response:
return _safe_fallback()
facts = _extract_new_facts(user_input, final_response, user_profile, farm_profile)
confidence = _compute_confidence(user_input, final_response)
style_shift = facts.pop("style_shift", None)
clarifying_question: str | None = None
from crews.agromatrix_crew.telemetry import tlog as _tlog
if confidence < 0.6:
clarifying_question = _build_clarifying_question(user_input, final_response, facts)
_tlog(logger, "reflection_done", confidence=round(confidence, 2),
clarifying=bool(clarifying_question), new_facts=list(facts.keys()))
logger.info(
"reflection_engine: low confidence=%.2f clarifying=%s",
confidence,
bool(clarifying_question),
)
else:
_tlog(logger, "reflection_done", confidence=round(confidence, 2),
clarifying=False, new_facts=list(facts.keys()))
logger.debug("reflection_engine: confidence=%.2f no clarification needed", confidence)
if facts:
logger.info("reflection_engine: new_facts=%s", list(facts.keys()))
return {
"new_facts": facts,
"style_shift": style_shift,
"confidence": confidence,
"clarifying_question": clarifying_question,
}
except Exception as exc:
from crews.agromatrix_crew.telemetry import tlog as _tlog
_tlog(logger, "reflection_skip", reason="error", error=str(exc))
logger.warning("reflection_engine: error (fallback): %s", exc)
return _safe_fallback()
finally:
_REFLECTING = False