Add topic/flavor categorization and category browsing
- New taxonomy module: single source of truth for 6 topics x 5 flavors, shared by the LLM response schema (enum-constrained) and validation. - Classifier now assigns one topic + one flavor per article; json_schema enums force valid values, with coercion as a safety net. - article_scores gains topic/flavor columns via an idempotent migration. - New 'list-category' command to browse by topic and/or flavor, ranked by composite score. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user