Technical Briefing: Horizon
The Problem
Horizon aggregates news from diverse sources—Hacker News, Reddit, Twitter/X, RSS, Telegram, GitHub, and OpenBB financial watchlists—then scores, deduplicates, and briefs them via AI. The core tension: each source amplifies noise, and without tight coupling between fetch, score, filter, and deliver stages, a change to one ripple widely through the pipeline. The hub module (src/models.py) is depended on by 33 other modules, meaning any structural change there broadcasts across the entire codebase.
What This Does
Horizon is a daily news briefing engine. It fetches items from configured sources, deduplicates by URL, scores each with an LLM, enriches with community discussion, filters by thresholds, and outputs ranked briefings in English and Chinese. Key files and their effects:
src/main.py— entry point; reaches 113 functions, orchestrates the full pipelinesrc/orchestrator.py—runreaches 149 functions; fetches all sources, determines time windows, progresses items through the pipeline, and writes summaries to disksrc/mcp/server.py— containshz_fetch_items(reaches 90 functions),hz_enrich_items,hz_filter_items, andhz_generate_summary; serves as the MCP adapter layersrc/models.py— hub module with 21 classes and 5 functions (validate_delivery,validate_platform, etc.); depended on by 33 modules, so changes here have blast-radius 33src/ai/client.py—completecalls a model for inference; 7 other classes (AnthropicClient, OpenAIClient, AzureOpenAIClient, MiniMaxClient)src/scrapers/— 8 scraper modules (rss.py,twitter.py,reddit.py,github.py,hackernews.py,openbb.py,ossinsight.py,telegram.py) each make outbound network callssrc/services/webhook.py— 31 functions; handles rendering, truncation, HTML stripping, and markdown conversion for delivery channelssrc/services/email.py—send_daily_summarysends via email; alsocheck_subscriptions
The pipeline flows: main → orchestrator.run → fetch from sources → deduplicate → score via ai/client.complete → enrich via ai/enricher.py → filter → summarize via mcp/server.hz_generate_summary → deliver via webhook or email.
How It Is Wired
Execution starts at src/main.py:34, which reaches 113 functions and is called from 1 place. The primary entry for the daily run is src/orchestrator.py:50 (run), which reaches 149 functions and is called by nothing else in the repo—it is the top-level orchestrator. From run, control flows to fetch_all_sources → _fetch_with_progress, then items move through hz_fetch_items → create_run → enrichment → filtering → summary generation.
Traced paths to effects outside the process:
main → load_config[filesystem viaconfig_path.read_text]run → check_subscriptions[network via mail.fetch]hz_enrich_items → enrich_items → save_items → write_json[filesystem viapath.write_text]hz_fetch_items → fetch_items → create_run[filesystem viarun_dir.mkdir]hz_generate_summary → generate_summary → save_summary[filesystem viapath.write_text]- Network calls radiate from 8 scraper files and
src/services/email.pyandsrc/services/webhook.py
The module graph shows src/orchestrator with indegree 1 and outdegree 19 (instability 0.95), making it the highest-change-risk point outside the hub. src/models has indegree 33 and outdegree 0 (instability 0)—pure sink, but any modification propagates to 33 callers.
How To Use It
Setup: This project uses uv for dependency management (per the badge in README and pyproject.toml). Install with:
uv sync
The pyproject.toml pins python^3.11 but has no uv.lock commit hash lock; the analysis flags this as a non-reproducible-build risk.
Configuration: Copy data/config.example.json to data/config.json and set required keys. Environment variables are documented in .env.example (file present at repo root). The config file is read in src/main.py via config_path.read_text.
Running it: The daily briefing is triggered by:
uv run src/main.py
or via the orchestrator:
uv run src/orchestrator.py
The GitHub Action .github/workflows/daily-summary.yml runs this on a schedule; it invokes the orchestrator which writes output and dispatches via configured channels.
Real-World Use
A product team wants a daily English/Chinese briefing filtered by subscriber interests. They configure data/config.json with their source list, set SCORING_MODEL=claude-3-5-sonnet, adjust MIN_SCORE=7, and point the webhook URL to their internal dashboard. The orchestrator runs nightly, produces a ranked list, enriches each item with community discussion summaries, and pushes the briefing via Feishu webhook (src/services/webhook.py) and email (src/services/email.py). If a story appears on both Reddit and Twitter, the deduplication in src/models.py merges them before scoring, so the subscriber sees one entry with consolidated context.
Code Health & Issues
The static analysis (19 findings, 4 high, 15 medium, 0 low) reports:
- [HIGH] Hub module —
src/models.pydepended on by 33 modules; high-blast-radius stability risk. Keep it small; move volatile logic out. - [HIGH/cognitive_load] Deep nesting x8 —
src/services/email.py,src/ai/utils.py,src/services/webhook.py; max indentation depth 11. Flatten with guard clauses. - [HIGH/cognitive_load] Oversized file x2 —
src/services/webhook.py(694 lines) andtests/test_webhook.py; hard to hold in one head. Split into cohesive units. - [HIGH/clarity] Duplicated code blocks — 32 repeated 6-line blocks across 14 files (
src/ai/analyzer.py,src/ai/enricher.py,src/ai/summarizer.py,src/orchestrator.py). Extract shared helpers. - [HIGH] Pin third-party GitHub Actions to commit SHA —
.github/workflowsusesastral-sh/setup-uv@v3,peaceiris/actions-gh-pages@v4; tags can move, so actions run with whatever owner last pushed. Replace@vNwith 40-char commit SHAs. - [MEDIUM] Broad exception handling x5 —
src/mcp/service.py,src/scrapers/rss.py,src/ai/enricher.py; bareexceptswallows errors indiscriminately. Catch specific exceptions; re-raise or log the rest. - [MEDIUM] File opened without context manager —
src/ai/enricher.py;open(...)not wrapped inwith. Usewith open(...) as f:for deterministic close. - [MEDIUM] High branching density —
src/ai/utils.py; 14 branch points over 45 lines. Decompose decision-heavy logic; consider strategy dispatch. - [MEDIUM] Enable Dependabot or Renovate — 1 manifest, no update bot. Without a bot, advisories sit unpatched.
- [MEDIUM] Pin container base image by digest —
Dockerfileusespython:3.11-slim(mutable tag). Usepython:3.11-slim@sha256:<digest>. - [MEDIUM] Gate pull requests on dependency vulnerability scan — no dependency scan in CI. Add
dependency-review-actiononpull_request. - [MEDIUM] Add non-root USER to image —
Dockerfilehas noUSERdirective; process runs as root. Create unprivileged user, chown needed paths, end withUSER. - [LOW] Set timeout-minutes on workflow jobs —
.github/workflows/daily-summary.ymldeclares no job timeout; wedged steps run to six-hour default.
The Bottom Line
Horizon is a functional, well-scoped news-briefing engine that chains scrapers, AI scoring, deduplication, and delivery into a daily pipeline. The codebase is Python-heavy with solid MCP adapter layering and clear entry-point separation. However, the hub module (src/models.py) and orchestrator (src/orchestrator.py) carry wide blast radii, and the hygiene gaps—mutable GitHub Action tags, no dependency scanning, a root-run container, and no lockfile—are fixable with modest, targeted changes. It’s a solid starting point for a technical team that needs a customizable news radar, provided they lock dependencies, flatten the nesting in the webhook and AI utils, and add a dependabot/renovate config.