""" 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)