"""Single source of truth for article topic/flavor categories. Both the LLM response schema (enum constraints) and the post-hoc validation in normalize_scores import from here, so the allowed values can never drift apart. Adjusting a category here + re-running `classify` is all it takes to reshape the browsable feeds. """ from __future__ import annotations # Topical axis: what the story is primarily about. TOPICS: dict[str, str] = { "science": "research, discoveries, space, physics, technology", "environment": "conservation, climate solutions, ecosystems, clean energy", "health": "medicine, wellbeing, mental health, public health", "community": "local action, humanitarian work, social progress, kindness, fair work", "culture": "arts, history, heritage, sport, human-interest", "animals": "wildlife, nature discoveries, charming animal stories", } # Tonal axis: why the story is worth surfacing in a calm, uplifting digest. FLAVORS: dict[str, str] = { "breakthrough": "a significant advance or innovation with clear public benefit", "discovery": "newly found or learned; calm and fascinating, low on agency", "solution": "people actively repairing, restoring, or solving a problem", "feelgood": "a heartwarming human, community, or kindness story", "perspective": "useful advice, insight, or framing the reader can apply", } DEFAULT_TOPIC = "science" DEFAULT_FLAVOR = "discovery" def coerce_topic(value: object) -> str: text = str(value or "").strip().lower() return text if text in TOPICS else DEFAULT_TOPIC def coerce_flavor(value: object) -> str: text = str(value or "").strip().lower() return text if text in FLAVORS else DEFAULT_FLAVOR def _bullet_list(mapping: dict[str, str]) -> str: return "\n".join(f"- {key}: {desc}" for key, desc in mapping.items()) def topics_prompt_block() -> str: return _bullet_list(TOPICS) def flavors_prompt_block() -> str: return _bullet_list(FLAVORS)