Deterministic first
The model is used only for semantic judgement: relevance, vertical, strength, and evidence. Dates, deduplication, clustering, and ranking are plain code, so they are fast, testable, and identical on every run.
A weekly business-signal pipeline for 537 enterprise accounts: collect from newsrooms and news, deduplicate syndicated copies, classify each event once, and rank the top two per vertical.
Every week a deal-origination team has to spot business signals (leadership changes, partnerships, acquisitions, investments) across hundreds of enterprise accounts, then pick the strongest two per industry vertical for a dashboard and a deck. By hand that means visiting every newsroom, searching the news, throwing away syndicated copies of the same story, and judging relevance one article at a time.
01
Each run loads the account list and the signal taxonomy and computes the previous Monday to Sunday in one timezone. Discovery looks back 14 days so late-listed releases are still caught; a late item is tagged, never dropped.
02
Every account gets a newsroom-discovery task and a news-search task in a persistent SQLite queue that workers claim atomically. Newsroom discovery diffs links against a cumulative URL inventory, so only new URLs move on; news-search links are decoded to the real publisher URL.
03
All fetches share a pool of at most four long-lived browsers, and HTML or PDF becomes Markdown. The publication date comes from a nine-step ladder, from structured data down to an LLM as the last resort, and every date records its source and confidence.
04
Each document gets a window status (in window, outside, unknown date, late arrival) and a company-match check. Rejections are kept as statuses rather than deleted, and a barrier waits for every collect task before moving on.
05
Four dedup layers run per account: canonical URL, content hash, title similarity, and near-duplicate content. Union-find then merges syndicated copies into a single event, with the official release as the canonical document.
06
The model is called once per event, not per article: ten copies of a story cost one call. A single response carries relevance, vertical, signal strength, a summary, verbatim evidence, and confidence, validated against the taxonomy with one repair turn and one fresh retry.
07
Low confidence, a vertical mismatch, or a failed validation sends the event to review. Analysts work in an exported spreadsheet with taxonomy dropdowns, and importing their decisions rebuilds the ranking.
08
A deterministic sort picks the top two per vertical (strength, confidence, source count, official source, recency), at most one per account. Reports are written atomically, and a run is only marked complete when report row counts match the database.
The model is used only for semantic judgement: relevance, vertical, strength, and evidence. Dates, deduplication, clustering, and ranking are plain code, so they are fast, testable, and identical on every run.
Phases are separated by barriers, every result is persisted as soon as it exists, and runs can pause and resume.
A one-week run over the first 20 accounts produced 591 tasks (569 succeeded, 22 failed), 383 documents, and 27 events, and reconciliation passed. That run stubbed the classifier, so it validates collection through clustering, not classification quality.
A browser per page was rejected for cost, and multi-tab control would have depended on undocumented driver internals. A pool of at most four long-lived browsers, one per worker, keeps fetching predictable.
A second path runs every Monday over about 760 companies and covers exactly the previous Monday to Sunday. It reads each company newsroom first, then news search, validates only the articles published in that week, and writes two reports: a valid-only deliverable and a full audit file where every rejected row keeps its reason.
About 45% of news-search rows were invalid: category pages, stock trackers, cookie pages, other companies. Adding movement keywords to the query made the search ignore the date range (0 of 89 results fell in the week), while a name-only exact-phrase search kept 93 to 98% in range. So category filtering moved to our side.
Chrome costs 5 to 60 seconds a page and some sites hang; plain HTTP takes about a second but can silently return a partial page. HTTP is trusted per site only once it has matched Chrome, and the site falls back to Chrome automatically when it stops matching.
Each article gets a fresh Gemini chat and must come back as JSON with company match, one of nine signal categories (or NONE), a framed headline, the news date, confidence, a reason, and a verbatim evidence sentence. A row is valid only when the company is a direct party, the category is a real signal, and the event is announced, not rumoured or recapped.
Every scraped URL is saved to state immediately, so a crash, Ctrl+C, or power cut loses nothing: re-running the same command skips finished companies and retries failed ones, and a flag skips discovery when only scraping is left.
Python · Botasaurus (Chrome pool) · trafilatura · PyMuPDF · gnews · RapidFuzz · Pydantic · SQLite (WAL) · openpyxl · Gemini · pytest