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

187 lines
7.3 KiB
Python
Raw 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.
"""
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:
"""Скоротити до 23 речень, прибрати надлишкові вступні фрази."""
# 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": "Відповідай стисло, 12 речення, без зайвих вступів.",
"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)