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:
+34
-5
@@ -7,6 +7,15 @@ import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .taxonomy import (
|
||||
FLAVORS,
|
||||
TOPICS,
|
||||
coerce_flavor,
|
||||
coerce_topic,
|
||||
flavors_prompt_block,
|
||||
topics_prompt_block,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_BASE_URL = "http://127.0.0.1:1234/v1"
|
||||
DEFAULT_MODEL = "gpt-oss"
|
||||
@@ -29,6 +38,8 @@ CLASSIFICATION_SCHEMA = {
|
||||
"novelty_score",
|
||||
"pr_risk_score",
|
||||
"accepted",
|
||||
"topic",
|
||||
"flavor",
|
||||
"reason_code",
|
||||
"reason_text",
|
||||
],
|
||||
@@ -41,6 +52,8 @@ CLASSIFICATION_SCHEMA = {
|
||||
"novelty_score": _SCORE_FIELD,
|
||||
"pr_risk_score": _SCORE_FIELD,
|
||||
"accepted": {"type": "boolean"},
|
||||
"topic": {"type": "string", "enum": list(TOPICS)},
|
||||
"flavor": {"type": "string", "enum": list(FLAVORS)},
|
||||
"reason_code": {"type": "string"},
|
||||
"reason_text": {"type": "string"},
|
||||
},
|
||||
@@ -61,8 +74,16 @@ Judge emotional aftertaste, not simple positivity. Accept stories that leave a r
|
||||
|
||||
Reject stories centered on fear, outrage, partisan conflict, crime, tragedy, disaster repetition, celebrity drama, market panic, or corporate PR without clear public benefit.
|
||||
|
||||
Also assign one topic and one flavor, choosing the single best fit.
|
||||
|
||||
Topic (what the story is about):
|
||||
{topics}
|
||||
|
||||
Flavor (why it belongs in a calm, uplifting digest):
|
||||
{flavors}
|
||||
|
||||
Return only JSON with this exact shape:
|
||||
{
|
||||
{{
|
||||
"constructive_score": 0,
|
||||
"cortisol_score": 0,
|
||||
"ragebait_score": 0,
|
||||
@@ -71,10 +92,12 @@ Return only JSON with this exact shape:
|
||||
"novelty_score": 0,
|
||||
"pr_risk_score": 0,
|
||||
"accepted": false,
|
||||
"topic": "one_of_the_allowed_topics",
|
||||
"flavor": "one_of_the_allowed_flavors",
|
||||
"reason_code": "short_snake_case",
|
||||
"reason_text": "one concise sentence"
|
||||
}
|
||||
"""
|
||||
}}
|
||||
""".format(topics=topics_prompt_block(), flavors=flavors_prompt_block())
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -218,6 +241,8 @@ def normalize_scores(data: dict, model_name: str) -> dict:
|
||||
"novelty_score": _bounded_int(data.get("novelty_score")),
|
||||
"pr_risk_score": _bounded_int(data.get("pr_risk_score")),
|
||||
"accepted": 1 if bool(data.get("accepted")) else 0,
|
||||
"topic": coerce_topic(data.get("topic")),
|
||||
"flavor": coerce_flavor(data.get("flavor")),
|
||||
"reason_code": str(data.get("reason_code") or "model_no_reason")[:120],
|
||||
"reason_text": str(data.get("reason_text") or "")[:1000],
|
||||
"model_name": model_name,
|
||||
@@ -230,9 +255,9 @@ def upsert_article_score(conn: sqlite3.Connection, article_id: int, scores: dict
|
||||
INSERT INTO article_scores (
|
||||
article_id, constructive_score, cortisol_score, ragebait_score,
|
||||
agency_score, human_benefit_score, novelty_score, pr_risk_score,
|
||||
accepted, reason_code, reason_text, model_name, scored_at
|
||||
accepted, topic, flavor, reason_code, reason_text, model_name, scored_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(article_id) DO UPDATE SET
|
||||
constructive_score = excluded.constructive_score,
|
||||
cortisol_score = excluded.cortisol_score,
|
||||
@@ -242,6 +267,8 @@ def upsert_article_score(conn: sqlite3.Connection, article_id: int, scores: dict
|
||||
novelty_score = excluded.novelty_score,
|
||||
pr_risk_score = excluded.pr_risk_score,
|
||||
accepted = excluded.accepted,
|
||||
topic = excluded.topic,
|
||||
flavor = excluded.flavor,
|
||||
reason_code = excluded.reason_code,
|
||||
reason_text = excluded.reason_text,
|
||||
model_name = excluded.model_name,
|
||||
@@ -257,6 +284,8 @@ def upsert_article_score(conn: sqlite3.Connection, article_id: int, scores: dict
|
||||
scores["novelty_score"],
|
||||
scores["pr_risk_score"],
|
||||
scores["accepted"],
|
||||
scores["topic"],
|
||||
scores["flavor"],
|
||||
scores["reason_code"],
|
||||
scores["reason_text"],
|
||||
scores["model_name"],
|
||||
|
||||
Reference in New Issue
Block a user