"""Select replayable demo scenarios from the shared scenario catalog.""" import re import unicodedata from dataclasses import dataclass from difflib import SequenceMatcher from functools import lru_cache from pathlib import Path import yaml _REPOSITORY_ROOT = Path(__file__).parents[4] SCENARIO_PATH = _REPOSITORY_ROOT / "eval" / "scenarios.yaml" FIXTURE_ROOT = _REPOSITORY_ROOT / "eval" / "fixtures" _WORD_PATTERN = re.compile(r"[a-z0-9]+") @dataclass(frozen=True) class Scenario: """One shared suggestion and its optional parent scenario.""" key: str chip: str query: str after: str | None = None @property def is_refinement(self) -> bool: """Return whether this scenario refines an earlier result.""" return self.after is not None @dataclass(frozen=True) class ScenarioMatch: """Describe which recorded scenario will answer a demo request.""" key: str is_exact: bool scenario_query: str chip: str @lru_cache(maxsize=1) def load_scenarios() -> tuple[Scenario, ...]: """Load and validate the shared scenario catalog once.""" payload = yaml.safe_load(SCENARIO_PATH.read_text(encoding="ascii")) if not isinstance(payload, dict) or payload.get("version") != 1: raise ValueError("Scenario catalog must have version 1") entries = payload.get("scenarios") if not isinstance(entries, list) or not entries: raise ValueError("Scenario catalog must contain scenarios") scenarios = tuple(_parse_scenario(entry) for entry in entries) keys = {scenario.key for scenario in scenarios} if len(keys) != len(scenarios): raise ValueError("Scenario keys must be unique") if any(scenario.after not in keys for scenario in scenarios if scenario.after is not None): raise ValueError("Scenario catalog contains an unknown parent") return scenarios def select_scenario(query: str) -> ScenarioMatch: """Select an exact scenario or the nearest available recorded fixture.""" scenarios = _replayable_scenarios() normalized_query = normalize_text(query) for scenario in scenarios: if normalized_query in {normalize_text(scenario.query), normalize_text(scenario.chip)}: return _to_match(scenario, is_exact=True) selected = max( scenarios, key=lambda scenario: max( SequenceMatcher(None, normalized_query, normalize_text(scenario.query)).ratio(), SequenceMatcher(None, normalized_query, normalize_text(scenario.chip)).ratio(), ), ) return _to_match(selected, is_exact=False) def select_replay_scenario( query: str, has_previous_recommendations: bool, ) -> ScenarioMatch: """Map a request with prior results to its recorded refinement when available.""" selected = select_scenario(query) scenarios = _replayable_scenarios() selected_scenario = next(scenario for scenario in scenarios if scenario.key == selected.key) if selected_scenario.is_refinement or not has_previous_recommendations: return selected refinement = next( (scenario for scenario in scenarios if scenario.after == selected_scenario.key), None, ) return _to_match(refinement, is_exact=False) if refinement is not None else selected def suggestion_items() -> list[dict[str, str]]: """Return chip and query pairs for non-refinement scenarios.""" return [ {"chip": scenario.chip, "query": scenario.query} for scenario in load_scenarios() if not scenario.is_refinement ] def normalize_text(value: str) -> str: """Normalize text for exact and similarity matching.""" decomposed = unicodedata.normalize("NFKD", value.casefold()) ascii_text = decomposed.encode("ascii", errors="ignore").decode("ascii") return " ".join(_WORD_PATTERN.findall(ascii_text)) @lru_cache(maxsize=1) def _replayable_scenarios() -> tuple[Scenario, ...]: fixture_keys = {path.name for path in FIXTURE_ROOT.iterdir() if path.is_dir()} scenarios = tuple(scenario for scenario in load_scenarios() if scenario.key in fixture_keys) if not scenarios: raise ValueError("Demo mode requires at least one scenario fixture") return scenarios def _parse_scenario(payload: object) -> Scenario: if not isinstance(payload, dict): raise ValueError("Each scenario must be a mapping") required = {"key", "chip", "query", "facets"} if not required <= set(payload): raise ValueError("Scenario is missing a required field") after = payload.get("after") if after is not None and not isinstance(after, str): raise ValueError("Scenario parent must be a key") return Scenario( key=_required_string(payload, "key"), chip=_required_string(payload, "chip"), query=_required_string(payload, "query"), after=after, ) def _required_string(payload: dict[object, object], key: str) -> str: value = payload.get(key) if not isinstance(value, str) or not value: raise ValueError(f"Scenario {key} must be a non-empty string") return value def _to_match(scenario: Scenario, is_exact: bool) -> ScenarioMatch: return ScenarioMatch( key=scenario.key, is_exact=is_exact, scenario_query=scenario.query, chip=scenario.chip, )