Files
PTSRaiAgent/app/api/demo.py
ElebobaandClaude Opus 5.5 25728c4b31
CI / ruff · pytest (pull_request) Successful in 1m25s
feat(backend): API stage 1 on real data instead of fixtures
Doctor list/card/alerts/ack/connect, dialog events/outbox/history and demo
controls now read the identity and clinical DBs. Rule-based discontinuation
risk index, trend, adherence; every doctor read is audited.

Refs #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:34:40 +03:00

90 lines
3.3 KiB
Python
Raw Permalink 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.
"""Demo-console helpers (#12, ADR-005): scripted personas, reset, connect links, outbox access.
Synthetic data only. Scenario files live in `data/synthetic/<slug>.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