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:
+61
-1
@@ -38,6 +38,12 @@ def main() -> None:
|
||||
source_parser = subparsers.add_parser("list-sources", help="Show configured sources")
|
||||
source_parser.add_argument("--active-only", action="store_true")
|
||||
|
||||
cat_parser = subparsers.add_parser("list-category", help="Browse articles by topic and/or flavor")
|
||||
cat_parser.add_argument("--topic", help="Filter by topic, e.g. science, environment, animals")
|
||||
cat_parser.add_argument("--flavor", help="Filter by flavor, e.g. breakthrough, discovery, feelgood")
|
||||
cat_parser.add_argument("--limit", type=int, default=20)
|
||||
cat_parser.add_argument("--all", action="store_true", help="Include not-accepted articles")
|
||||
|
||||
subparsers.add_parser("source-report", help="Show source-level ingestion and scoring stats")
|
||||
|
||||
runs_parser = subparsers.add_parser("list-runs", help="Show recent ingest runs")
|
||||
@@ -90,6 +96,8 @@ def main() -> None:
|
||||
list_recent(conn, limit=args.limit, accepted_only=args.accepted_only)
|
||||
elif args.command == "list-sources":
|
||||
list_sources(conn, active_only=args.active_only)
|
||||
elif args.command == "list-category":
|
||||
list_category(conn, topic=args.topic, flavor=args.flavor, limit=args.limit, accepted_only=not args.all)
|
||||
elif args.command == "source-report":
|
||||
source_report(conn)
|
||||
elif args.command == "list-runs":
|
||||
@@ -109,7 +117,10 @@ def main() -> None:
|
||||
)
|
||||
for article_id, scores in results:
|
||||
accepted = "yes" if scores["accepted"] else "no"
|
||||
print(f"[{article_id}] accepted={accepted} reason={scores['reason_code']}")
|
||||
print(
|
||||
f"[{article_id}] accepted={accepted} {scores['topic']}/{scores['flavor']} "
|
||||
f"reason={scores['reason_code']}"
|
||||
)
|
||||
print(f" {scores['reason_text']}")
|
||||
if args.dry_run:
|
||||
print("Dry run only; database was not updated.")
|
||||
@@ -179,6 +190,55 @@ def list_recent(conn: sqlite3.Connection, limit: int, accepted_only: bool) -> No
|
||||
print(f" {row['canonical_url']}")
|
||||
|
||||
|
||||
def list_category(
|
||||
conn: sqlite3.Connection,
|
||||
topic: str | None,
|
||||
flavor: str | None,
|
||||
limit: int,
|
||||
accepted_only: bool,
|
||||
) -> None:
|
||||
clauses = []
|
||||
params: list = []
|
||||
if accepted_only:
|
||||
clauses.append("s.accepted = 1")
|
||||
if topic:
|
||||
clauses.append("s.topic = ?")
|
||||
params.append(topic.lower())
|
||||
if flavor:
|
||||
clauses.append("s.flavor = ?")
|
||||
params.append(flavor.lower())
|
||||
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
||||
params.append(limit)
|
||||
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT
|
||||
a.id, a.title, a.canonical_url, a.published_at,
|
||||
src.name AS source_name,
|
||||
s.topic, s.flavor, s.accepted,
|
||||
s.constructive_score, s.cortisol_score, s.reason_code,
|
||||
(s.constructive_score + s.agency_score + s.human_benefit_score + src.trust_score
|
||||
- s.cortisol_score - s.ragebait_score - s.pr_risk_score) AS rank_score
|
||||
FROM articles a
|
||||
JOIN sources src ON src.id = a.source_id
|
||||
JOIN article_scores s ON s.article_id = a.id
|
||||
{where}
|
||||
ORDER BY rank_score DESC, COALESCE(a.published_at, a.discovered_at) DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
params,
|
||||
).fetchall()
|
||||
|
||||
label = " / ".join(filter(None, [topic, flavor])) or "all categories"
|
||||
print(f"{label} ({len(rows)} shown)")
|
||||
for row in rows:
|
||||
accepted = "" if row["accepted"] else " [not accepted]"
|
||||
print(f"[{row['id']}] {row['topic']}/{row['flavor']} | {row['source_name']}{accepted}")
|
||||
print(f" {row['title']}")
|
||||
print(f" score={row['rank_score']} reason={row['reason_code']}")
|
||||
print(f" {row['canonical_url']}")
|
||||
|
||||
|
||||
def llm_client_from_args(args: argparse.Namespace) -> LocalModelClient:
|
||||
client = LocalModelClient.from_env()
|
||||
if getattr(args, "base_url", None):
|
||||
|
||||
Reference in New Issue
Block a user