"""Demo-console helpers (#12, ADR-005): scripted personas, reset, connect links, outbox access. Synthetic data only. Scenario files live in `data/synthetic/.jsonl` (#18); a patient is a scripted persona when the transliterated first word of the display name equals the slug ("Андрей С." → `andrey.jsonl`). """ import inspect import logging from importlib import import_module, util from pathlib import Path from app.config import get_settings from app.integrations.base import MemoryOutbox, Outbox, integrations log = logging.getLogger(__name__) SEED_MODULE = "scripts.seed_demo" CONSOLE_PATH = "/console" _TRANSLIT = { "а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "е": "e", "ё": "e", "ж": "zh", "з": "z", "и": "i", "й": "y", "к": "k", "л": "l", "м": "m", "н": "n", "о": "o", "п": "p", "р": "r", "с": "s", "т": "t", "у": "u", "ф": "f", "х": "kh", "ц": "ts", "ч": "ch", "ш": "sh", "щ": "shch", "ъ": "", "ы": "y", "ь": "", "э": "e", "ю": "yu", "я": "ya", } # fmt: skip def persona_slug(display_name: str) -> str: first = (display_name.split() or [""])[0].lower() return "".join(_TRANSLIT.get(ch, ch if ch.isascii() and ch.isalnum() else "") for ch in first) def scenario_files(data_dir: Path | None = None) -> dict[str, str]: """slug → scenario path relative to the project root, e.g. {"andrey": "data/synthetic/andrey.jsonl"}.""" root = data_dir or get_settings().data_dir folder = root / "synthetic" if not folder.is_dir(): return {} return {f.stem: f"data/synthetic/{f.name}" for f in sorted(folder.glob("*.jsonl"))} async def bot_username() -> str | None: """Username of the running Telegram bot (deep links); None when the bot is not started (no TG_BOT_TOKEN).""" from app.channels import telegram bot = getattr(telegram, "_bot", None) if bot is None: return None try: me = await bot.me() # aiogram caches getMe except Exception as exc: # noqa: BLE001 — network / auth errors: fall back to the web console link log.warning("bot username unavailable error=%s", type(exc).__name__) return None return me.username or None async def deep_link(code: str) -> str: username = await bot_username() return f"https://t.me/{username}?start={code}" if username else f"{CONSOLE_PATH}?code={code}" def memory_outbox() -> MemoryOutbox | None: """The in-memory outbox polled by the web console, also when wrapped (Telegram keeps it as `fallback`).""" box: Outbox | None = integrations.outbox for _ in range(5): if isinstance(box, MemoryOutbox): return box box = getattr(box, "fallback", None) return None async def reset_demo() -> bool: """Clock reset is done by the caller; re-seed via `scripts.seed_demo.reset()` (#18) when it exists.""" box = memory_outbox() if box is not None: box._q.clear() try: spec = util.find_spec(SEED_MODULE) except ModuleNotFoundError: # no `scripts` package on the path (e.g. installed app without scripts/) spec = None if spec is None: return False reset = getattr(import_module(SEED_MODULE), "reset", None) if reset is None: return False result = reset() if inspect.isawaitable(result): await result return True