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
This commit is contained in:
186
crews/agromatrix_crew/style_adapter.py
Normal file
186
crews/agromatrix_crew/style_adapter.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Style Adapter для Степана.
|
||||
|
||||
adapt_response_style(response, user_profile) → str
|
||||
|
||||
Не змінює зміст відповіді, лише форму:
|
||||
concise → скорочує, прибирає пояснення
|
||||
checklist → переформатовує у маркери
|
||||
analytical → додає блок "Причина / Наслідок"
|
||||
detailed → дозволяє довшу форму (без змін)
|
||||
conversational → за замовчуванням, без змін
|
||||
|
||||
Стиль визначається:
|
||||
1. Явні слова користувача ("коротко", "списком", ...)
|
||||
2. Поле user_profile["style"]
|
||||
|
||||
Fail-safe: будь-який виняток → повертає оригінальну відповідь.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ─── Sentence splitter ───────────────────────────────────────────────────────
|
||||
|
||||
_SENT_SPLIT_RE = re.compile(r'(?<=[.!?])\s+')
|
||||
|
||||
|
||||
def _split_sentences(text: str) -> list[str]:
|
||||
return [s.strip() for s in _SENT_SPLIT_RE.split(text.strip()) if s.strip()]
|
||||
|
||||
|
||||
# ─── Style transformers ──────────────────────────────────────────────────────
|
||||
|
||||
def _to_concise(text: str) -> str:
|
||||
"""Скоротити до 2–3 речень, прибрати надлишкові вступні фрази."""
|
||||
# Remove common filler openings
|
||||
filler_re = re.compile(
|
||||
r'^(звісно[,!]?\s*|звичайно[,!]?\s*|добре[,!]?\s*|зрозуміло[,!]?\s*'
|
||||
r'|окей[,!]?\s*|ок[,!]?\s*|чудово[,!]?\s*|ось[,!]?\s*|так[,!]?\s*)',
|
||||
re.IGNORECASE | re.UNICODE,
|
||||
)
|
||||
text = filler_re.sub('', text).strip()
|
||||
|
||||
sentences = _split_sentences(text)
|
||||
if len(sentences) <= 3:
|
||||
return text
|
||||
# Keep first 3 meaningful sentences
|
||||
short = ' '.join(sentences[:3])
|
||||
if len(sentences) > 3:
|
||||
short += ' …'
|
||||
return short
|
||||
|
||||
|
||||
def _to_checklist(text: str) -> str:
|
||||
"""
|
||||
Переформатовує відповідь у маркований список.
|
||||
Якщо вже є маркери — повертає без змін.
|
||||
"""
|
||||
if re.search(r'^\s*[-•*]\s', text, re.MULTILINE):
|
||||
return text # already formatted
|
||||
|
||||
sentences = _split_sentences(text)
|
||||
if len(sentences) < 2:
|
||||
return text # too short to convert
|
||||
|
||||
items = '\n'.join(f'• {s}' for s in sentences)
|
||||
return items
|
||||
|
||||
|
||||
def _to_analytical(text: str) -> str:
|
||||
"""
|
||||
Додає короткий блок «Чому це важливо:» якщо відповідь досить довга.
|
||||
Не дублює зміст — тільки додає структуру.
|
||||
"""
|
||||
sentences = _split_sentences(text)
|
||||
if len(sentences) < 3:
|
||||
return text
|
||||
|
||||
# First 2 sentences — основа; решта — обґрунтування
|
||||
main = ' '.join(sentences[:2])
|
||||
reason = ' '.join(sentences[2:4])
|
||||
result = main
|
||||
if reason:
|
||||
result += f'\n\n*Чому це важливо:* {reason}'
|
||||
return result
|
||||
|
||||
|
||||
# ─── Style detection from text ───────────────────────────────────────────────
|
||||
|
||||
_STYLE_SIGNAL: dict[str, list[str]] = {
|
||||
"concise": ["коротко", "без деталей", "стисло", "коротку відповідь", "кратко"],
|
||||
"checklist": ["списком", "маркерами", "у списку", "по пунктах", "пунктами"],
|
||||
"analytical": ["аналіз", "причини", "наслідки", "детальний аналіз", "розбери"],
|
||||
"detailed": ["детально", "докладно", "розгорнуто", "повністю", "докладну"],
|
||||
}
|
||||
|
||||
|
||||
def detect_style_from_text(text: str) -> str | None:
|
||||
"""Визначити бажаний стиль з тексту повідомлення."""
|
||||
tl = text.lower()
|
||||
for style, signals in _STYLE_SIGNAL.items():
|
||||
if any(s in tl for s in signals):
|
||||
return style
|
||||
return None
|
||||
|
||||
|
||||
# ─── Main adapter ────────────────────────────────────────────────────────────
|
||||
|
||||
def adapt_response_style(response: str, user_profile: dict | None) -> str:
|
||||
"""
|
||||
Адаптувати відповідь під стиль користувача.
|
||||
|
||||
Якщо user_profile відсутній або style не визначено — повертає оригінал.
|
||||
Fail-safe: будь-який виняток → повертає оригінал.
|
||||
"""
|
||||
try:
|
||||
if not response or not user_profile:
|
||||
return response
|
||||
|
||||
style = user_profile.get("style") or "conversational"
|
||||
|
||||
if style == "concise":
|
||||
adapted = _to_concise(response)
|
||||
elif style == "checklist":
|
||||
adapted = _to_checklist(response)
|
||||
elif style == "analytical":
|
||||
adapted = _to_analytical(response)
|
||||
else:
|
||||
# "detailed" and "conversational" — no transformation
|
||||
adapted = response
|
||||
|
||||
if adapted != response:
|
||||
logger.debug("style_adapter: style=%s original_len=%d adapted_len=%d", style, len(response), len(adapted))
|
||||
|
||||
return adapted
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("style_adapter: failed (returning original): %s", exc)
|
||||
return response
|
||||
|
||||
|
||||
def build_style_prefix(user_profile: dict | None) -> str:
|
||||
"""
|
||||
Сформувати prefix для system prompt Степана з урахуванням профілю.
|
||||
Використовується у _stepan_light_response і фінальній задачі Deep mode.
|
||||
"""
|
||||
if not user_profile:
|
||||
return ""
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
name = user_profile.get("name")
|
||||
if name:
|
||||
parts.append(f"Користувача звати {name}.")
|
||||
|
||||
role = user_profile.get("role", "unknown")
|
||||
role_labels = {
|
||||
"owner": "власник/керівник господарства",
|
||||
"agronomist": "агроном",
|
||||
"operator": "оператор",
|
||||
"mechanic": "механік",
|
||||
}
|
||||
if role in role_labels:
|
||||
parts.append(f"Його роль: {role_labels[role]}.")
|
||||
|
||||
style = user_profile.get("style", "conversational")
|
||||
style_instructions = {
|
||||
"concise": "Відповідай стисло, 1–2 речення, без зайвих вступів.",
|
||||
"checklist": "Якщо доречно — структуруй відповідь у маркований список.",
|
||||
"analytical": "Якщо доречно — виділи причину і наслідок.",
|
||||
"detailed": "Можеш відповідати розгорнуто.",
|
||||
"conversational": "Говори природно, живою мовою.",
|
||||
}
|
||||
if style in style_instructions:
|
||||
parts.append(style_instructions[style])
|
||||
|
||||
summary = user_profile.get("interaction_summary")
|
||||
if summary:
|
||||
parts.append(f"Контекст про користувача: {summary}")
|
||||
|
||||
return " ".join(parts)
|
||||
Reference in New Issue
Block a user