Task 5: demand-aware ingestion funnel#678
Open
mircealungu wants to merge 2 commits into
Open
Conversation
Make the crawler's expensive LLM simplification demand-driven instead of supply-driven. Previously simplification ran until a static per-language cap (50/day/topic for da/fr/de, 20 otherwise) and then stopped — so dormant languages were simplified at the same rate as busy ones and first-in-feed-order won the quota. New zeeguu/core/content_retriever/funnel.py: - compute_demand_surface(): the distinct (language, topic) buckets active readers (last_seen <= 30d) actually want, deduped across the cohort; a reader with no topic subscription lends demand to the whole language. - FunnelBudget: per-(language, topic) quota that scales 20->50 with reader count, plus a pilot-light floor (<=5 distinct topics/day) for dormant *supported* languages so a new learner / the kiosk never hits an empty feed. - Pre-download title triage: skip a feed entirely when its language is already satisfied for the day, or keep only the best-N titles (cheap LLM ranker with a safe first-N fallback) when headroom is limited. article_downloader.py + crawl.py thread a FunnelBudget instead of the old static-cap counts dict. Legacy download_from_feed callers pass no budget, so their behaviour is unchanged. No-topic articles are now simplified only when the language has all-topic readers (old code always simplified them). Promotion-on-join (backfill.py): subscribing to a topic or switching learned language fires a background backfill that simplifies a few EXISTING recent raw articles for the newly-demanded bucket (dormant buckets already hold raw, un-simplified articles — the crawl stores them, only skips the LLM), so the reader doesn't wait for the next crawl. No-ops if the bucket already has fresh inventory. Wired into the subscribe and learned-language endpoints. 31 tests in zeeguu/core/test/test_funnel.py (in-memory SQLite). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reshness
Three correctness bugs from code review of the demand-aware funnel:
1. Pilot-light floor re-fired on every intra-day crawl. FunnelBudget seeded
today's demand counts but not floor_topics_used, so each hourly crawl process
re-floored a fresh set of ~5 topics for a dormant language — ~5*N/day instead
of ~5. FunnelBudget now derives floor_topics_used from today's counts for
languages with no demand (correct by construction for any caller).
2. _apply_title_triage claimed "never raises" but had no try/except, so a
titleless RSS item (or any error) aborted the entire feed instead of falling
back to the full list. Wrapped the body; also use feed_item.get("title", "").
3. backfill freshness/recency keyed off published_time, which simplified
children inherit from the (backdatable) parent — so the "already stocked"
guard under-counted and the candidate query missed recently-crawled but
old-dated raw articles. Both now filter/order on crawled_at (ingestion time).
+5 regression tests (36 total).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Makes the crawler's expensive LLM simplification demand-driven instead of supply-driven, and gives newly-demanded buckets an immediate head start. All 6 phases of the Task 5 design.
Problem
Simplification ran until a static per-language cap (50/day/topic for da/fr/de, 20 otherwise), then stopped. So dormant languages were simplified at the same rate as busy ones, and whichever articles appeared first in feed order won the quota (first-N, not best-N). Evidence from the week-28 report: ~370 crawled per 1 opened.
What changed
New
zeeguu/core/content_retriever/funnel.pycompute_demand_surface()— the distinct(language, topic)buckets that active readers (last_seen ≤ 30d) actually want, deduped across the cohort. A reader with no topic subscription reads the whole language, so they lend demand to every topic in it.FunnelBudget— per-(language, topic)quota that scales 20→50 with reader count, plus a pilot-light floor (≤5 distinct topics/day) for dormant supported languages so a brand-new learner (or the Romanian kiosk) never opens an empty feed.article_downloader.py+crawl.pythread aFunnelBudgetinstead of the old static-cap counts dict. Legacydownload_from_feedcallers pass no budget → behaviour unchanged. No-topic articles are now simplified only when the language has all-topic readers (old code always simplified them = pure waste).Promotion-on-join —
backfill.pySubscribing to a topic or switching learned language fires a background backfill that simplifies a few existing recent raw articles for the newly-demanded bucket. Dormant buckets already hold raw, un-simplified articles (the crawl stores them, only skips the LLM), so this is fast and download-free — no mini-crawl. No-ops if the bucket already has fresh inventory. Wired into the subscribe and learned-language endpoints via
run_in_background.Phase mapping
/simplify_article= the tail)Tests
31 tests in
zeeguu/core/test/test_funnel.py(in-memory SQLite): quota scaling, demand surface, gate/floor/no-topic paths, headroom, triage selection + fallback, backfill inventory queries. Regression-checkedtest_retrieve_and_compute/test_article/test_feed/test_endpoint_names.Reviewer notes / conscious choices
last_seen ≤ 30d(chosen over open-based). Implication: German (many recently-seen users) stays "in demand" — this does not reproduce the open-based "German → 0" reduction; topic-dedup and dormant-language savings still apply. The definition is isolated in one function (_active_reader_rows) for an easy switch later.crawl_stats.pydashboard still displays the now-superseded static "cap" (legacy constants kept alive only for it) — follow-up.🤖 Generated with Claude Code