Skip to content

Task 5: demand-aware ingestion funnel#678

Open
mircealungu wants to merge 2 commits into
masterfrom
wt/ingestion-funnel
Open

Task 5: demand-aware ingestion funnel#678
mircealungu wants to merge 2 commits into
masterfrom
wt/ingestion-funnel

Conversation

@mircealungu

Copy link
Copy Markdown
Member

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.py

  • compute_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.
  • Pre-download title triage — before paying any readability/download cost: skip a feed entirely when its language is already satisfied for the day, or keep only the best-N titles when headroom is limited. Cheap LLM ranker with a safe first-N fallback (only runs when candidates exceed the keep count), so triage can only reduce waste, never drop a must-have article.

article_downloader.py + crawl.py thread a FunnelBudget instead of the old static-cap counts dict. Legacy download_from_feed callers 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.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 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

Phase Status
1. Demand surface + quota done
2. Two-tier pilot-light floor done
3. Title-triage best-N done (pre-download LLM triage)
4. Pre-simplify cache already satisfied (crawl-time simplification = the cache; /simplify_article = the tail)
5. Promotion-on-join done
6. Romanian floor satisfied by the floor covering every supported language

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-checked test_retrieve_and_compute / test_article / test_feed / test_endpoint_names.

Reviewer notes / conscious choices

  • Active reader = 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.
  • Not yet validated on a real crawl / real subscribe flow — logic is unit-tested with the LLM mocked; the LLM ranker and background backfill need a live run.
  • crawl_stats.py dashboard still displays the now-superseded static "cap" (legacy constants kept alive only for it) — follow-up.
  • Backfill has no dedup guard against concurrent triggers (conservative caps mitigate); a brand-new never-crawled language finds no raw inventory and relies on the floor + next crawl.

🤖 Generated with Claude Code

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>
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

ArchLens detected architectural changes in the following views:
diff
diff
diff

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant