"""Template texts from `config/texts/phrases.yaml`: base meaning for `phrase`, mock output, fallbacks.""" from collections.abc import Mapping from functools import lru_cache from pathlib import Path from typing import Any import yaml from pydantic import BaseModel, Field from app.config import get_settings DEFAULT_INTENT = "default" class SummaryTexts(BaseModel): empty: str labels: dict[str, str] = Field(default_factory=dict) class PhraseTexts(BaseModel): phrases: dict[str, str] summary: SummaryTexts class _BlankMissing(dict[str, Any]): def __missing__(self, key: str) -> str: return "" @lru_cache def load_texts(path: Path | None = None) -> PhraseTexts: path = path or get_settings().config_dir / "texts" / "phrases.yaml" texts = PhraseTexts.model_validate(yaml.safe_load(path.read_text(encoding="utf-8"))) if DEFAULT_INTENT not in texts.phrases: raise ValueError(f"{path}: phrases.{DEFAULT_INTENT} is required") return texts def render_phrase(intent: str, context: Mapping[str, Any]) -> str: phrases = load_texts().phrases template = phrases.get(intent, phrases[DEFAULT_INTENT]) return " ".join(template.format_map(_BlankMissing(context)).split()) def _fmt(value: Any) -> str: if isinstance(value, bool): return "да" if value else "нет" if isinstance(value, list | tuple): sep = " → " if all(isinstance(v, int | float) for v in value) else ", " return sep.join(_fmt(v) for v in value) if isinstance(value, Mapping): return ", ".join(f"{k}: {_fmt(v)}" for k, v in value.items()) return str(value) def render_summary(structured: Mapping[str, Any]) -> str: """Deterministic doctor summary: one "label: value" line per non-empty field, known labels first.""" texts = load_texts().summary labels = texts.labels ordered = [k for k in labels if k in structured] + [k for k in structured if k not in labels] lines = [ f"{labels.get(key, key)}: {_fmt(structured[key])}" for key in ordered if structured[key] not in (None, "", [], {}) ] return "\n".join(lines) if lines else texts.empty